|
| 1 | +import { describe, it, expect } from 'vitest' |
| 2 | +import { Contract, RpcProvider } from 'starknet' |
| 3 | +import abi from '../abis/STARKNET_HTLC.json' with { type: 'json' } |
| 4 | +import { buildUserLockTx } from '../client/wallet/buildUserLockTx' |
| 5 | +import { buildRefundTx } from '../client/wallet/buildRefundTx' |
| 6 | +import { buildRedeemSolverTx } from '../client/wallet/buildRedeemSolverTx' |
| 7 | +import { ZERO_ADDRESS } from '../constants' |
| 8 | + |
| 9 | +// Deployed Train contract (Starknet Sepolia). Only used offline to build an ABI-aware |
| 10 | +// Contract for `populate`, which is the source of truth for calldata ordering. |
| 11 | +const CONTRACT = '0x4ae1ae0dd1dd01be306725ab8de15707241284c8dd49cbaf37bcf845bf2d20b' |
| 12 | +const USER = '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' |
| 13 | +const SOLVER = '0x00abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdef01' |
| 14 | +const TOKEN = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d' |
| 15 | + |
| 16 | +const contract = () => |
| 17 | + new Contract({ abi: abi as any, address: CONTRACT, providerOrAccount: new RpcProvider({ nodeUrl: 'http://offline' }) }) |
| 18 | +const felts = (cd: unknown) => (cd as string[]).map(v => BigInt(v)) |
| 19 | + |
| 20 | +const baseUserLockParams: any = { |
| 21 | + sourceChain: 'starknet-sepolia', destinationChain: 'ethereum-sepolia', |
| 22 | + amount: '1.5', destinationAmount: '1000000', |
| 23 | + sourceAsset: { contract: TOKEN, decimals: 18 }, |
| 24 | + destinationAsset: { contract: '0x0000000000000000000000000000000000000000' }, |
| 25 | + srcSolverAddress: SOLVER, atomicContract: CONTRACT, sourceAddress: USER, |
| 26 | + destinationAddress: '0x0000000000000000000000000000000000000001', |
| 27 | + payoutCurve: '', quoteExpiry: 1899999999, |
| 28 | + rewardToken: '', rewardRecipient: '', rewardTimelockDelta: 0, |
| 29 | + timelockDelta: 3600, hashlock: '0x1234abcd', nonce: 1721600000000, |
| 30 | +} |
| 31 | + |
| 32 | +describe('Starknet buildUserLockTx', () => { |
| 33 | + it('serializes UserLockParams in the exact new contract order (no sender; refund_to/payout_curve/payout_curve_data)', () => { |
| 34 | + const built = felts(buildUserLockTx(baseUserLockParams).calldata) |
| 35 | + // ABI-driven reference: pass raw values, let populate encode per the deployed ABI. |
| 36 | + const ref = felts(contract().populate('user_lock', [ |
| 37 | + { hashlock: 0x1234abcdn, amount: 1500000000000000000n, reward_amount: 0n, |
| 38 | + timelock_delta: 3600, reward_timelock_delta: 0, quote_expiry: 1899999999, |
| 39 | + recipient: SOLVER, token: TOKEN, reward_token: '', reward_recipient: '', |
| 40 | + src_chain: 'starknet-sepolia', refund_to: USER, payout_curve: ZERO_ADDRESS, payout_curve_data: '' }, |
| 41 | + { dst_chain: 'ethereum-sepolia', dst_address: '0x0000000000000000000000000000000000000001', |
| 42 | + dst_amount: 1000000n, dst_token: '0x0000000000000000000000000000000000000000' }, |
| 43 | + String(1721600000000), '', |
| 44 | + ]).calldata) |
| 45 | + // UserLockParams + dst_chain occupy the first 28 felts; the dst_address/dst_token |
| 46 | + // ByteArrays use the app's byteArrayFromString convention (unchanged) so compare the |
| 47 | + // struct this change owns. |
| 48 | + expect(built.slice(0, 28)).toEqual(ref.slice(0, 28)) |
| 49 | + }) |
| 50 | + |
| 51 | + it('defaults payout_curve to ZERO_ADDRESS and refund_to to the user when no curve is set', () => { |
| 52 | + const built = buildUserLockTx(baseUserLockParams) |
| 53 | + expect(built.entrypoint).toBe('user_lock') |
| 54 | + // felt 21 is payout_curve (see layout above) → 0 when no curve |
| 55 | + expect(felts(built.calldata)[21]).toBe(0n) |
| 56 | + }) |
| 57 | + |
| 58 | + it('passes a provided payout curve through', () => { |
| 59 | + const curve = '0x27e92c85cf5da7861549ceba60096737a773f9b94f49046604ff0a0035cc351' |
| 60 | + const built = felts(buildUserLockTx({ ...baseUserLockParams, payoutCurve: curve }).calldata) |
| 61 | + expect(built[21]).toBe(BigInt(curve)) |
| 62 | + }) |
| 63 | +}) |
| 64 | + |
| 65 | +describe('Starknet buildRefundTx', () => { |
| 66 | + it('matches refund_user(hashlock) ABI encoding', () => { |
| 67 | + const built = felts(buildRefundTx({ contractAddress: CONTRACT, id: '0x1234abcd' } as any).calldata) |
| 68 | + const ref = felts(contract().populate('refund_user', [0x1234abcdn]).calldata) |
| 69 | + expect(built).toEqual(ref) |
| 70 | + }) |
| 71 | +}) |
| 72 | + |
| 73 | +describe('Starknet buildRedeemSolverTx', () => { |
| 74 | + it('matches redeem_solver(hashlock, index, secret) ABI encoding', () => { |
| 75 | + const built = felts(buildRedeemSolverTx({ contractAddress: CONTRACT, id: '0x1234abcd', index: 2, secret: '0xdeadbeef' } as any).calldata) |
| 76 | + const ref = felts(contract().populate('redeem_solver', [0x1234abcdn, 2n, 0xdeadbeefn]).calldata) |
| 77 | + expect(built).toEqual(ref) |
| 78 | + }) |
| 79 | + |
| 80 | + it('defaults index to 1 when omitted', () => { |
| 81 | + const built = felts(buildRedeemSolverTx({ contractAddress: CONTRACT, id: '0x1234abcd', secret: '0xdeadbeef' } as any).calldata) |
| 82 | + const ref = felts(contract().populate('redeem_solver', [0x1234abcdn, 1n, 0xdeadbeefn]).calldata) |
| 83 | + expect(built).toEqual(ref) |
| 84 | + }) |
| 85 | +}) |
0 commit comments