diff --git a/modules/contracts/deploy/deploy.ts b/modules/contracts/deploy/deploy.ts index a2453d7d2..dcd33ff09 100644 --- a/modules/contracts/deploy/deploy.ts +++ b/modules/contracts/deploy/deploy.ts @@ -68,6 +68,7 @@ const func: DeployFunction = async () => { ["ChannelFactory", ["ChannelMastercopy", Zero]], ["HashlockTransfer", []], ["Withdraw", []], + ["CrosschainTransfer", []], ["TransferRegistry", []], ["TestToken", []], ]; @@ -93,7 +94,7 @@ const func: DeployFunction = async () => { // Default: run standard migration } else { - log.info(`Running testnet migration`); + log.info(`Running standard migration`); for (const row of standardMigration) { const name = row[0] as string; const args = row[1] as Array; @@ -101,6 +102,7 @@ const func: DeployFunction = async () => { } await registerTransfer("Withdraw", deployer); await registerTransfer("HashlockTransfer", deployer); + await registerTransfer("CrosschainTransfer", deployer); } if ([1337, 5].includes(network.config.chainId ?? 0)) { diff --git a/modules/contracts/src.sol/transferDefinitions/CrosschainTransfer.sol b/modules/contracts/src.sol/transferDefinitions/CrosschainTransfer.sol new file mode 100644 index 000000000..58ce3c529 --- /dev/null +++ b/modules/contracts/src.sol/transferDefinitions/CrosschainTransfer.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.7.1; +pragma experimental ABIEncoderV2; + +import "./TransferDefinition.sol"; +import "../lib/LibChannelCrypto.sol"; + +/// @title CrosschainTransfer +/// @author Connext +/// @notice This contract burns the initiator's funds if a mutually signed +/// transfer can be generated + +contract CrosschainTransfer is TransferDefinition { + using LibChannelCrypto for bytes32; + + struct TransferState { + bytes initiatorSignature; + address initiator; + address responder; + bytes32 data; + uint256 nonce; // Included so that each transfer commitment has a unique hash. + uint256 fee; + address callTo; + bytes callData; + bytes32 lockHash; + } + + struct TransferResolver { + bytes responderSignature; + bytes32 preImage; + } + + // Provide registry information. + string public constant override Name = "CrosschainTransfer"; + string public constant override StateEncoding = + "tuple(bytes initiatorSignature, address initiator, address responder, bytes32 data, uint256 nonce, uint256 fee, address callTo, bytes callData, bytes32 lockHash)"; + string public constant override ResolverEncoding = + "tuple(bytes responderSignature, bytes32 preImage)"; + + function EncodedCancel() external pure override returns (bytes memory) { + TransferResolver memory resolver; + resolver.responderSignature = new bytes(65); + resolver.preImage = bytes32(0); + return abi.encode(resolver); + } + + function create(bytes calldata encodedBalance, bytes calldata encodedState) + external + pure + override + returns (bool) + { + // Get unencoded information. + TransferState memory state = abi.decode(encodedState, (TransferState)); + Balance memory balance = abi.decode(encodedBalance, (Balance)); + + // Ensure data and nonce provided. + require(state.data != bytes32(0), "CrosschainTransfer: EMPTY_DATA"); + require(state.nonce != uint256(0), "CrosschainTransfer: EMPTY_NONCE"); + + // Initiator balance can be 0 for crosschain contract calls + require( + state.fee <= balance.amount[0], + "CrosschainTransfer: INSUFFICIENT_BALANCE" + ); + + // Recipient balance must be 0. + require( + balance.amount[1] == 0, + "CrosschainTransfer: NONZERO_RECIPIENT_BALANCE" + ); + + // Valid lockHash to secure funds must be provided. + require( + state.lockHash != bytes32(0), + "CrosschainTransfer: EMPTY_LOCKHASH" + ); + + require( + state.data.checkSignature( + state.initiatorSignature, + state.initiator + ), + "CrosschainTransfer: INVALID_INITIATOR_SIG" + ); + + require( + state.initiator != address(0) && state.responder != address(0), + "CrosschainTransfer: EMPTY_SIGNERS" + ); + + // Valid initial transfer state + return true; + } + + function resolve( + bytes calldata encodedBalance, + bytes calldata encodedState, + bytes calldata encodedResolver + ) external pure override returns (Balance memory) { + TransferState memory state = abi.decode(encodedState, (TransferState)); + TransferResolver memory resolver = + abi.decode(encodedResolver, (TransferResolver)); + Balance memory balance = abi.decode(encodedBalance, (Balance)); + + require( + state.data.checkSignature( + resolver.responderSignature, + state.responder + ), + "CrosschainTransfer: INVALID_RESPONDER_SIG" + ); + + // Check hash for normal payment unlock. + bytes32 generatedHash = sha256(abi.encode(resolver.preImage)); + require( + state.lockHash == generatedHash, + "CrosschainTransfer: INVALID_PREIMAGE" + ); + + // Reduce CrosschainTransfer amount to optional fee. + // It's up to the offchain validators to ensure that the + // CrosschainTransfer commitment takes this fee into account. + balance.amount[1] = state.fee; + balance.amount[0] = 0; + + return balance; + } +} diff --git a/modules/engine/src/errors.ts b/modules/engine/src/errors.ts index d635865ac..f78860bc2 100644 --- a/modules/engine/src/errors.ts +++ b/modules/engine/src/errors.ts @@ -106,6 +106,8 @@ export class ParameterConversionError extends EngineError { FeeGreaterThanAmount: "Fees charged are greater than amount", NoOp: "Cannot create withdrawal with 0 amount and no call", WithdrawToZero: "Cannot withdraw to AddressZero", + ChannelNotFound: "Channel not found", + TransferNotFound: "Transfer not found", } as const; constructor( diff --git a/modules/engine/src/index.ts b/modules/engine/src/index.ts index dfb841b5c..a4a467ae8 100644 --- a/modules/engine/src/index.ts +++ b/modules/engine/src/index.ts @@ -28,9 +28,10 @@ import { MinimalTransaction, WITHDRAWAL_RESOLVED_EVENT, VectorErrorJson, + FullTransferState, } from "@connext/vector-types"; import { - generateMerkleTreeData, + recoverAddressFromChannelMessage, validateChannelUpdateSignatures, getSignerAddressFromPublicIdentifier, getRandomBytes32, @@ -41,6 +42,7 @@ import { import pino from "pino"; import Ajv from "ajv"; import { Evt } from "evt"; +import { BigNumber } from "@ethersproject/bignumber"; import { version } from "../package.json"; @@ -51,7 +53,7 @@ import { convertSetupParams, convertWithdrawParams, } from "./paramConverter"; -import { setupEngineListeners } from "./listeners"; +import { isCrosschainTransfer, setupEngineListeners } from "./listeners"; import { getEngineEvtContainer, withdrawRetryForTransferId, addTransactionToCommitment } from "./utils"; import { sendIsAlive } from "./isAlive"; import { WithdrawCommitment } from "@connext/vector-contracts"; @@ -885,11 +887,17 @@ export class VectorEngine implements IVectorEngine { ); } - const transferRes = await this.getTransferState({ transferId: params.transferId }); - if (transferRes.isError) { - return Result.fail(transferRes.getError()!); + let transfer: FullTransferState | undefined; + try { + transfer = await this.store.getTransferState(params.transferId); + } catch (e) { + return Result.fail( + new RpcError(RpcError.reasons.TransferNotFound, params.channelAddress ?? "", this.publicIdentifier, { + transferId: params.transferId, + getTransferStateError: jsonifyError(e), + }), + ); } - const transfer = transferRes.getValue(); if (!transfer) { return Result.fail( new RpcError(RpcError.reasons.TransferNotFound, params.channelAddress ?? "", this.publicIdentifier, { diff --git a/modules/engine/src/listeners.ts b/modules/engine/src/listeners.ts index ae6ac20d1..40c7e3522 100644 --- a/modules/engine/src/listeners.ts +++ b/modules/engine/src/listeners.ts @@ -1036,6 +1036,23 @@ export const isWithdrawTransfer = async ( return Result.ok(transfer.transferDefinition === definition); }; +export const isCrosschainTransfer = async ( + transfer: FullTransferState, + chainAddresses: ChainAddresses, + chainService: IVectorChainReader, +): Promise> => { + const crosschainInfo = await chainService.getRegisteredTransferByName( + TransferNames.CrosschainTransfer, + chainAddresses[transfer.chainId].transferRegistryAddress, + transfer.chainId, + ); + if (crosschainInfo.isError) { + return Result.fail(crosschainInfo.getError()!); + } + const { definition } = crosschainInfo.getValue(); + return Result.ok(transfer.transferDefinition === definition); +}; + export const resolveWithdrawal = async ( channelState: FullChannelState, transfer: FullTransferState, diff --git a/modules/engine/src/paramConverter.ts b/modules/engine/src/paramConverter.ts index b9a48c95b..21119cb5f 100644 --- a/modules/engine/src/paramConverter.ts +++ b/modules/engine/src/paramConverter.ts @@ -1,5 +1,9 @@ import { WithdrawCommitment } from "@connext/vector-contracts"; -import { getRandomBytes32, getSignerAddressFromPublicIdentifier } from "@connext/vector-utils"; +import { + getRandomBytes32, + getSignerAddressFromPublicIdentifier, + recoverAddressFromChannelMessage, +} from "@connext/vector-utils"; import { CreateTransferParams, ResolveTransferParams, @@ -21,12 +25,15 @@ import { IMessagingService, DEFAULT_FEE_EXPIRY, SetupParams, + IVectorChainService, + IEngineStore, } from "@connext/vector-types"; import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; import { getAddress } from "@ethersproject/address"; import { ParameterConversionError } from "./errors"; +import { isCrosschainTransfer } from "./listeners"; export async function convertSetupParams( params: EngineParams.Setup, @@ -198,12 +205,80 @@ export async function convertConditionalTransferParams( }); } -export function convertResolveConditionParams( +export async function convertResolveConditionParams( params: EngineParams.ResolveTransfer, transfer: FullTransferState, -): Result { + signer: IChannelSigner, + chainAddresses: ChainAddresses, + chainService: IVectorChainReader, + store: IEngineStore, +): Promise> { const { channelAddress, transferResolver, meta } = params; + // special case for crosschain transfer + // we need to generate a separate sig for withdrawal commitment since the transfer resolver may have gotten forwarded + // and needs to be regenerated for this leg of the transfer + const isCrossChain = await isCrosschainTransfer(transfer, chainAddresses, chainService); + if (isCrossChain.getValue()) { + // first check if the provided sig is valid. in the case of the receiver directly resolving the withdrawal, it will + // be valid already + let channel: FullChannelState | undefined; + try { + channel = await store.getChannelState(transfer.channelAddress); + } catch (e) { + return Result.fail( + new ParameterConversionError( + ParameterConversionError.reasons.ChannelNotFound, + transfer.channelAddress, + signer.publicIdentifier, + { + getChannelStateError: jsonifyError(e), + }, + ), + ); + } + if (!channel) { + return Result.fail( + new ParameterConversionError( + ParameterConversionError.reasons.ChannelNotFound, + transfer.channelAddress, + signer.publicIdentifier, + ), + ); + } + const { + transferState: { nonce, initiatorSignature, fee, callTo, callData }, + balance, + } = transfer; + const withdrawalAmount = balance.amount.reduce((prev, curr) => prev.add(curr), BigNumber.from(0)).sub(fee); + const commitment = new WithdrawCommitment( + channel.channelAddress, + channel.alice, + channel.bob, + signer.address, + transfer.assetId, + withdrawalAmount.toString(), + nonce, + callTo, + callData, + ); + let recovered: string; + try { + recovered = await recoverAddressFromChannelMessage(commitment.hashToSign(), transferResolver.responderSignature); + } catch (e) { + recovered = e.message; + } + + // if it is not valid, regenerate the sig, otherwise use the provided one + if (recovered !== channel.alice && recovered !== channel.bob) { + // Generate your signature on the withdrawal commitment + transferResolver.responderSignature = await signer.signMessage(commitment.hashToSign()); + } + await commitment.addSignatures(initiatorSignature, transferResolver.responderSignature); + // Store the double signed commitment + await store.saveWithdrawalCommitment(transfer.transferId, commitment.toJson()); + } + return Result.ok({ channelAddress, transferId: transfer.transferId, diff --git a/modules/engine/src/testing/index.spec.ts b/modules/engine/src/testing/index.spec.ts index ddc741dcb..ed54d58e7 100644 --- a/modules/engine/src/testing/index.spec.ts +++ b/modules/engine/src/testing/index.spec.ts @@ -10,6 +10,10 @@ import { getRandomBytes32, mkPublicIdentifier, mkAddress, + createTestFullHashlockTransferState, + createTestFullCrosschainTransferState, + createTestChannelState, + ChannelSigner, } from "@connext/vector-utils"; import Sinon from "sinon"; @@ -35,12 +39,12 @@ describe("VectorEngine", () => { const validAddress = mkAddress("0xc"); const invalidAddress = "abc"; - let storeService: IEngineStore; + let storeService: Sinon.SinonStubbedInstance; let chainService: Sinon.SinonStubbedInstance; beforeEach(() => { - storeService = Sinon.createStubInstance(MemoryStoreService, { - getChannelStates: Promise.resolve([]), - }); + storeService = Sinon.createStubInstance(MemoryStoreService); + storeService.getChannelStates.resolves([]); + storeService.getTransferState.resolves(createTestFullHashlockTransferState()); chainService = Sinon.createStubInstance(VectorChainService); chainService.getChainProviders.returns(Result.ok(env.chainProviders)); diff --git a/modules/engine/src/testing/paramConverter.spec.ts b/modules/engine/src/testing/paramConverter.spec.ts index fe07a2ae2..ed2123be1 100644 --- a/modules/engine/src/testing/paramConverter.spec.ts +++ b/modules/engine/src/testing/paramConverter.spec.ts @@ -15,6 +15,8 @@ import { DEFAULT_CHANNEL_TIMEOUT, SetupParams, IVectorChainReader, + IEngineStore, + IChannelSigner, } from "@connext/vector-types"; import { createTestChannelState, @@ -27,12 +29,15 @@ import { ChannelSigner, NatsMessagingService, mkPublicIdentifier, + MemoryStoreService, + createTestFullCrosschainTransferState, + getRandomChannelSigner, + mkSig, } from "@connext/vector-utils"; import { expect } from "chai"; -import Sinon from "sinon"; +import Sinon, { SinonStub, stub } from "sinon"; import { VectorChainReader, WithdrawCommitment } from "@connext/vector-contracts"; import { getAddress } from "@ethersproject/address"; -import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; import { @@ -42,6 +47,7 @@ import { convertWithdrawParams, } from "../paramConverter"; import { ParameterConversionError } from "../errors"; +import * as listeners from "../listeners"; import { env } from "./env"; @@ -67,12 +73,16 @@ describe("ParamConverter", () => { let signerA: Sinon.SinonStubbedInstance; let signerB: Sinon.SinonStubbedInstance; let messaging: Sinon.SinonStubbedInstance; + let store: Sinon.SinonStubbedInstance; const setDefaultStubs = (registryInfo: RegisteredTransfer = transferRegisteredInfo) => { chainReader = Sinon.createStubInstance(VectorChainReader); signerA = Sinon.createStubInstance(ChannelSigner); signerB = Sinon.createStubInstance(ChannelSigner); messaging = Sinon.createStubInstance(NatsMessagingService); + store = Sinon.createStubInstance(MemoryStoreService); + store.getTransferState.resolves(createTestFullHashlockTransferState()); + store.getChannelState.resolves(createTestChannelState("create").channel); signerA.signMessage.resolves("success"); signerB.signMessage.resolves("success"); @@ -551,6 +561,7 @@ describe("ParamConverter", () => { }); describe("convertResolveConditionParams", () => { + let isCrosschainTransfer: SinonStub; const generateParams = (): EngineParams.ResolveTransfer => { setDefaultStubs(); return { @@ -565,12 +576,19 @@ describe("ParamConverter", () => { }; }; + beforeEach(() => { + isCrosschainTransfer = stub(listeners, "isCrosschainTransfer"); + isCrosschainTransfer.resolves(Result.ok(false)); + }); + it("should work", async () => { const params = generateParams(); const transferState: FullTransferState = createTestFullHashlockTransferState({ channelAddress: params.channelAddress, }); - const ret: ResolveTransferParams = convertResolveConditionParams(params, transferState).getValue(); + const ret: ResolveTransferParams = ( + await convertResolveConditionParams(params, transferState, signerA, chainAddresses, chainReader as any, store) + ).getValue(); expect(ret).to.deep.eq({ channelAddress: params.channelAddress, transferId: transferState.transferId, @@ -583,6 +601,126 @@ describe("ParamConverter", () => { }, }); }); + + describe("is crosschain transfer", () => { + let transfer: FullTransferState; + let channel: FullChannelState; + let alice: IChannelSigner; + let bob: ChannelSigner; + let defaultParams: any; + let responderSignature: string; + let initiatorSignature: string; + beforeEach(async () => { + defaultParams = generateParams(); + isCrosschainTransfer.resolves(Result.ok(true)); + alice = getRandomChannelSigner(); + bob = getRandomChannelSigner(); + channel = createTestChannelState("create").channel; + channel.alice = alice.address; + channel.bob = bob.address; + transfer = createTestFullCrosschainTransferState(); + + const commitment = new WithdrawCommitment( + channel.channelAddress, + channel.alice, + channel.bob, + channel.bob, + transfer.assetId, + transfer.balance.amount[0], + "1", + ); + console.log("commitment:", commitment.toJson()); + initiatorSignature = await alice.signMessage(commitment.hashToSign()); + responderSignature = await bob.signMessage(commitment.hashToSign()); + + transfer.transferState.initiatorSignature = initiatorSignature; + transfer.transferState.callData = undefined; + transfer.transferState.callTo = undefined; + transfer.transferState.data = commitment.hashToSign(); + transfer.transferState.initiator = channel.alice; + transfer.transferState.responder = channel.bob; + + store.getTransferState.resolves(transfer); + store.getChannelState.resolves(channel); + }); + + it("should fail if get channel errors", async () => { + store.getChannelState.rejects("Blah"); + + const res = await convertResolveConditionParams( + { + transferId: getRandomBytes32(), + channelAddress: mkAddress(), + transferResolver: { preImage: getRandomBytes32() }, + }, + transfer, + alice, + chainAddresses, + chainReader as any, + store, + ); + expect(res.isError).to.be.true; + expect(res.getError()?.message).to.eq(ParameterConversionError.reasons.ChannelNotFound); + }); + + it("should fail if channel not found", async () => { + store.getChannelState.resolves(undefined); + const res = await convertResolveConditionParams( + { + transferId: getRandomBytes32(), + channelAddress: mkAddress(), + transferResolver: { preImage: getRandomBytes32() }, + }, + transfer, + alice, + chainAddresses, + chainReader as any, + store, + ); + expect(res.isError).to.be.true; + expect(res.getError()?.message).to.eq(ParameterConversionError.reasons.ChannelNotFound); + }); + + it("should use provided resolver sig if it's valid", async () => { + const preImage = getRandomBytes32(); + + const res = await convertResolveConditionParams( + { + transferId: getRandomBytes32(), + channelAddress: mkAddress(), + transferResolver: { preImage, responderSignature }, + }, + transfer, + bob, + chainAddresses, + chainReader as any, + store, + ); + + expect(res.isError).to.be.false; + expect(res.getValue()).to.deep.contain({ transferResolver: { preImage, responderSignature } }); + }); + + it("should use regenerate resolver sig if it's not valid", async () => { + const preImage = getRandomBytes32(); + + const res = await convertResolveConditionParams( + { + transferId: getRandomBytes32(), + channelAddress: mkAddress(), + transferResolver: { preImage, responderSignature: mkSig() }, + }, + transfer, + bob, + chainAddresses, + chainReader as any, + store, + ); + + expect(res.isError).to.be.false; + expect(res.getValue()).to.deep.contain({ transferResolver: { preImage, responderSignature } }); + }); + }); }); describe("convertWithdrawParams", () => { diff --git a/modules/types/src/transferDefinitions/crosschain.ts b/modules/types/src/transferDefinitions/crosschain.ts new file mode 100644 index 000000000..027a914d3 --- /dev/null +++ b/modules/types/src/transferDefinitions/crosschain.ts @@ -0,0 +1,38 @@ +import { HexString, SignatureString, Address, Bytes32 } from "../basic"; +import { tidy } from "../utils"; + +export const CrosschainTransferName = "CrosschainTransfer"; + +export type CrosschainTransferState = { + initiatorSignature: SignatureString; + initiator: Address; + responder: Address; + data: Bytes32; + nonce: string; + fee: string; + callTo: Address; + callData: string; + lockHash: HexString; +}; + +export type CrosschainTransferResolver = { + responderSignature: SignatureString; + preImage: HexString; +}; + +export const CrosschainTransferStateEncoding = tidy(`tuple( + bytes initiatorSignature, + address initiator, + address responder, + bytes32 data, + uint256 nonce, + uint256 fee, + address callTo, + bytes callData, + bytes32 lockHash +)`); + +export const CrosschainTransferResolverEncoding = tidy(`tuple( + bytes responderSignature, + bytes32 preImage +)`); diff --git a/modules/types/src/transferDefinitions/index.ts b/modules/types/src/transferDefinitions/index.ts index 08616acaf..d6222928f 100644 --- a/modules/types/src/transferDefinitions/index.ts +++ b/modules/types/src/transferDefinitions/index.ts @@ -1,3 +1,4 @@ export * from "./hashlockTransfer"; export * from "./shared"; export * from "./withdraw"; +export * from "./crosschain"; diff --git a/modules/types/src/transferDefinitions/shared.ts b/modules/types/src/transferDefinitions/shared.ts index a9a5b1d38..391d38c0c 100644 --- a/modules/types/src/transferDefinitions/shared.ts +++ b/modules/types/src/transferDefinitions/shared.ts @@ -17,23 +17,27 @@ import { WithdrawState, WithdrawStateEncoding, } from "./withdraw"; +import { CrosschainTransferName, CrosschainTransferResolver, CrosschainTransferState } from "./crosschain"; // Must be updated when adding a new transfer export const TransferNames = { [HashlockTransferName]: HashlockTransferName, [WithdrawName]: WithdrawName, + [CrosschainTransferName]: CrosschainTransferName, } as const; // Must be updated when adding a new transfer export interface TransferResolverMap { [HashlockTransferName]: HashlockTransferResolver; [WithdrawName]: WithdrawResolver; + [CrosschainTransferName]: CrosschainTransferResolver; } // Must be updated when adding a new transfer export interface TransferStateMap { [HashlockTransferName]: HashlockTransferState; [WithdrawName]: WithdrawState; + [CrosschainTransferName]: CrosschainTransferState; } // Must be updated when adding a new transfer diff --git a/modules/utils/src/test/transfers.ts b/modules/utils/src/test/transfers.ts index 4922b51d4..a5b08d8c3 100644 --- a/modules/utils/src/test/transfers.ts +++ b/modules/utils/src/test/transfers.ts @@ -7,13 +7,16 @@ import { HashlockTransferStateEncoding, HashlockTransferResolverEncoding, FullChannelState, + CrosschainTransferResolverEncoding, + CrosschainTransferState, + CrosschainTransferStateEncoding, } from "@connext/vector-types"; import { sha256 as soliditySha256 } from "@ethersproject/solidity"; import { getRandomBytes32 } from "../hexStrings"; import { hashTransferState } from "../transfers"; -import { mkAddress, mkHash, mkBytes32, mkPublicIdentifier } from "./util"; +import { mkAddress, mkHash, mkBytes32, mkPublicIdentifier, mkSig } from "./util"; export const createTestHashlockTransferState = (overrides: Partial = {}): TransferState => { return { @@ -118,3 +121,74 @@ export function createTestFullHashlockTransferState( ...channelOverrides, }; } + +export const createTestCrosschainTransferState = ( + overrides: Partial = {}, +): CrosschainTransferState => { + return { + callData: mkHash(), + callTo: mkAddress(), + data: mkBytes32(), + fee: "0", + initiator: mkAddress("0xA"), + responder: mkAddress("0xB"), + initiatorSignature: mkSig(), + lockHash: mkBytes32("0xaaa"), + nonce: "1", + ...overrides, + }; +}; + +export function createTestFullCrosschainTransferState( + overrides: Partial = {}, + channel?: FullChannelState, +): FullTransferState { + // get overrides/defaults values + const { assetId, preImage, expiry, meta, ...core } = overrides; + + // Taken from onchain defs + const transferEncodings = [CrosschainTransferStateEncoding, CrosschainTransferResolverEncoding]; + const transferResolver = { preImage: preImage ?? getRandomBytes32() }; + const transferState = createTestCrosschainTransferState({ + lockHash: soliditySha256(["bytes32"], [transferResolver.preImage]), + }); + + // get default values + const defaults = { + assetId: assetId ?? mkAddress(), + chainId: 2, + channelAddress: mkAddress("0xccc"), + channelFactoryAddress: mkAddress("0xaaaaddddffff"), + balance: overrides.balance ?? { to: [mkAddress("0x111"), mkAddress("0x222")], amount: ["13", "0"] }, + initialStateHash: hashTransferState(transferState, transferEncodings[0]), + meta: meta ?? { super: "cool stuff", routingId: mkHash("0xaabb") }, + transferDefinition: mkAddress("0xdef"), + transferEncodings, + transferId: getRandomBytes32(), + transferResolver, + transferState, + transferTimeout: DEFAULT_TRANSFER_TIMEOUT.toString(), + initiator: overrides.balance?.to[0] ?? mkAddress("0x111"), + responder: overrides.balance?.to[1] ?? mkAddress("0x222"), + inDispute: false, + initiatorIdentifier: overrides.initiatorIdentifier ?? channel?.aliceIdentifier ?? mkPublicIdentifier("vector111"), + responderIdentifier: overrides.responderIdentifier ?? channel?.bobIdentifier ?? mkPublicIdentifier("vector222"), + channelNonce: channel?.nonce ?? 9, + }; + + const channelOverrides = channel + ? { + inDispute: channel.inDispute, + aliceIdentifier: defaults.initiatorIdentifier, + bobIdentifier: defaults.responderIdentifier, + ...channel.networkContext, + ...channel.latestUpdate, + } + : {}; + + return { + ...defaults, + ...core, + ...channelOverrides, + }; +}