| 1002 | } |
| 1003 | |
| 1004 | async proposeSwap(deal: SwapDeal, options?: ProposeSwapOptions): Promise<SwapProposalResult> { |
| 1005 | this.ensureNotDestroyed(); |
| 1006 | this.ensureReady(); |
| 1007 | const deps = this.deps!; |
| 1008 | |
| 1009 | // Step 2: Validate deal fields (§17.1) |
| 1010 | if (typeof deal.partyA !== 'string' || deal.partyA.length === 0) { |
| 1011 | throw new SphereError('SWAP_INVALID_DEAL: partyA is required', 'SWAP_INVALID_DEAL'); |
| 1012 | } |
| 1013 | if (typeof deal.partyB !== 'string' || deal.partyB.length === 0) { |
| 1014 | throw new SphereError('SWAP_INVALID_DEAL: partyB is required', 'SWAP_INVALID_DEAL'); |
| 1015 | } |
| 1016 | if (deal.partyA.toLowerCase() === deal.partyB.toLowerCase()) { |
| 1017 | throw new SphereError('SWAP_INVALID_DEAL: partyA and partyB must be different', 'SWAP_INVALID_DEAL'); |
| 1018 | } |
| 1019 | const currencyRe = /^[A-Za-z0-9]{1,20}$/; |
| 1020 | if (typeof deal.partyACurrency !== 'string' || !currencyRe.test(deal.partyACurrency)) { |
| 1021 | throw new SphereError('SWAP_INVALID_DEAL: partyACurrency must be 1-20 alphanumeric characters', 'SWAP_INVALID_DEAL'); |
| 1022 | } |
| 1023 | if (typeof deal.partyBCurrency !== 'string' || !currencyRe.test(deal.partyBCurrency)) { |
| 1024 | throw new SphereError('SWAP_INVALID_DEAL: partyBCurrency must be 1-20 alphanumeric characters', 'SWAP_INVALID_DEAL'); |
| 1025 | } |
| 1026 | if (deal.partyACurrency === deal.partyBCurrency) { |
| 1027 | throw new SphereError('SWAP_INVALID_DEAL: currencies must differ', 'SWAP_INVALID_DEAL'); |
| 1028 | } |
| 1029 | const amountRe = /^[1-9][0-9]*$/; |
| 1030 | if (typeof deal.partyAAmount !== 'string' || !amountRe.test(deal.partyAAmount)) { |
| 1031 | throw new SphereError('SWAP_INVALID_DEAL: partyAAmount must be a positive integer string', 'SWAP_INVALID_DEAL'); |
| 1032 | } |
| 1033 | if (typeof deal.partyBAmount !== 'string' || !amountRe.test(deal.partyBAmount)) { |
| 1034 | throw new SphereError('SWAP_INVALID_DEAL: partyBAmount must be a positive integer string', 'SWAP_INVALID_DEAL'); |
| 1035 | } |
| 1036 | if (typeof deal.timeout !== 'number' || !Number.isInteger(deal.timeout) || deal.timeout < 60 || deal.timeout > 86400) { |
| 1037 | throw new SphereError('SWAP_INVALID_DEAL: timeout must be an integer between 60 and 86400 seconds', 'SWAP_INVALID_DEAL'); |
| 1038 | } |
| 1039 | |
| 1040 | // Step 3: Resolve escrow address |
| 1041 | const escrowAddress = deal.escrowAddress ?? this.config.defaultEscrowAddress; |
| 1042 | if (!escrowAddress) { |
| 1043 | throw new SphereError( |
| 1044 | 'No escrow address: provide deal.escrowAddress or configure defaultEscrowAddress', |
| 1045 | 'SWAP_INVALID_DEAL', |
| 1046 | ); |
| 1047 | } |
| 1048 | const escrowPeer = await deps.resolve(escrowAddress); |
| 1049 | if (!escrowPeer) { |
| 1050 | throw new SphereError(`Cannot resolve escrow address: ${escrowAddress}`, 'SWAP_RESOLVE_FAILED'); |
| 1051 | } |
| 1052 | |
| 1053 | // Step 4: Resolve party addresses |
| 1054 | const peerA = await deps.resolve(deal.partyA); |
| 1055 | if (!peerA) { |
| 1056 | throw new SphereError(`Cannot resolve party A address: ${deal.partyA}`, 'SWAP_RESOLVE_FAILED'); |
| 1057 | } |
| 1058 | const peerB = await deps.resolve(deal.partyB); |
| 1059 | if (!peerB) { |
| 1060 | throw new SphereError(`Cannot resolve party B address: ${deal.partyB}`, 'SWAP_RESOLVE_FAILED'); |
| 1061 | } |