Repository Public
1import { describe, expect, it } from 'vitest';2import fc from 'fast-check';3import { ed25519 } from '@noble/curves/ed25519.js';4import { hmac } from '@noble/hashes/hmac.js';5import { sha512 } from '@noble/hashes/sha2.js';6import { sha3_256 } from '@noble/hashes/sha3.js';7import {8 Account, AccountAddress, AuthenticationKey, ChainId, Deserializer, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature, EntryFunction, RawTransaction, SignedTransaction, SimpleTransaction,9 TransactionAuthenticatorEd25519, TransactionPayloadEntryFunction, U64, generateSignedTransaction, generateSignedTransactionForSimulation, generateSigningMessageForTransaction, generateUserTransactionHash,10} from '@aptos-labs/ts-sdk';11import {12 APTOS_MAINNET_CHAIN_ID, aptosAddressOf, aptosAptStore, aptosDecodeAddress, aptosPlugin, aptosRawBytes, aptosSignedTx, aptosSimulationTx, aptosTxid, buildAptosTx, canonicalBytes, DEFAULT_POLICY,13 intentHash, isValidAddress, NETWORKS, normalizeAddress, pluginFor, toHex, type Intent, type KeyHandle, type NetworkDef,14} from '../src/index.ts';1516const net = NETWORKS.aptos as NetworkDef;17const hexToBytes = (h: string) => Uint8Array.from(Buffer.from(h.replace(/^0x/, ''), 'hex'));18const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';19// BIP-39 seed of the public test mnemonic (empty passphrase)20const SEED = hexToBytes('5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4');21// Account 0 of the public test mnemonic in Petra / @aptos-labs/ts-sdk (m/44'/637'/0'/0'/0'). The account exists on mainnet.22const VECTOR = { address: '0xeb663b681209e7087d681c5d3eed12aaa8e1915e7c87794542c3f96e94b3d3bf', publicKey: '0xa686f0309ab80312979606cfccc10ea2740147ae6888351488d11c46f08fbf60' };2324/** SLIP-0010 ed25519 (hardened only), independent of the signer's implementation. */25function slip10(seed: Uint8Array, path: string): Uint8Array {26 let I = hmac(sha512, new TextEncoder().encode('ed25519 seed'), seed);27 for (const seg of path.split('/').slice(1)) {28 if (!seg.endsWith("'")) throw new Error('hardened only');29 const data = new Uint8Array(37);30 data.set(I.subarray(0, 32), 1);31 new DataView(data.buffer).setUint32(33, (Number(seg.slice(0, -1)) | 0x80000000) >>> 0);32 I = hmac(sha512, I.subarray(32), data);33 }34 return I.slice(0, 32);35}36const PRIV = slip10(SEED, aptosPlugin.path(net));37const PUB = ed25519.getPublicKey(PRIV);38const FROM = VECTOR.address;39const TO = aptosAddressOf(ed25519.getPublicKey(slip10(SEED, "m/44'/637'/1'/0'/0'")));40const THIRD = aptosAddressOf(ed25519.getPublicKey(slip10(SEED, "m/44'/637'/2'/0'/0'")));41const key: KeyHandle = { curve: 'ed25519', publicKey: PUB, signMessage: (m) => ed25519.sign(m, PRIV) };42const sdkAccount = () => Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(PRIV, false), legacy: true });4344const now = 1_800_000_000;45const base: Intent = {46 v: 1, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a11', keyId: 'spw-0123456789abcdef', createdAt: now, expiresAt: now + 120, policyVersion: 3,47 network: 'aptos', chainRef: '1', asset: 'native', symbol: 'APT', decimals: 8, amount: '125000000', from: FROM, to: TO, maxFee: '1000000',48 x: { sequence_number: '129', max_gas_amount: '8122', gas_unit_price: '100', expiration_timestamp_secs: String(now + 180) },49};50const withX = (x: Record<string, string>, over: Partial<Intent> = {}): Intent => ({ ...base, ...over, x: { ...base.x!, ...x } });5152/** The same transaction built by the official SDK from the same fields. */53function sdkRaw(i: Intent): RawTransaction {54 const x = i.x!;55 const payload = new TransactionPayloadEntryFunction(EntryFunction.build('0x1::aptos_account', 'transfer', [], [AccountAddress.from(i.to), new U64(BigInt(i.amount))]));56 return new RawTransaction(AccountAddress.from(i.from), BigInt(x.sequence_number!), payload, BigInt(x.max_gas_amount!), BigInt(x.gas_unit_price!), BigInt(x.expiration_timestamp_secs!), new ChainId(1));57}5859describe('aptos: registration', () => {60 it('is registered with the pinned identity, derivation, limits and two sources', () => {61 expect(pluginFor(net)).toBe(aptosPlugin);62 expect([net.family, net.id, net.chainRef, net.coinType, net.memo, net.activation, net.native.symbol, net.native.decimals]).toEqual(['aptos', 'aptos', '1', 637, undefined, undefined, 'APT', 8]);63 expect(APTOS_MAINNET_CHAIN_ID).toBe(1);64 expect(aptosPlugin.curve).toBe('ed25519');65 expect(aptosPlugin.path(net)).toBe("m/44'/637'/0'/0'/0'");66 expect(() => aptosPlugin.path({ ...net, coinType: 501 })).toThrow();67 expect(new Set(net.rpc.map((u) => new URL(u).host)).size).toBe(2);68 expect(DEFAULT_POLICY.perTx['aptos:APT']).toBe('8800000000');69 expect(DEFAULT_POLICY.daily['aptos:APT']).toBe('17600000000');70 expect(DEFAULT_POLICY.maxFee.aptos).toBe('10000000');71 });72});7374describe('aptos: keys and addresses', () => {75 it('derives the account of the public test mnemonic exactly like the official SDK (Petra path)', () => {76 expect('0x' + toHex(PUB)).toBe(VECTOR.publicKey);77 expect(aptosPlugin.address(net, PUB)).toBe(VECTOR.address);78 for (const k of [0, 1, 2]) {79 const path = `m/44'/637'/${k}'/0'/0'`;80 const sdk = Account.fromDerivationPath({ path, mnemonic: MNEMONIC, legacy: true });81 const pub = ed25519.getPublicKey(slip10(SEED, path));82 expect(toHex(pub)).toBe(toHex(sdk.publicKey.toUint8Array()));83 expect(aptosAddressOf(pub)).toBe(sdk.accountAddress.toString());84 expect(aptosAddressOf(pub)).toBe(sdk.accountAddress.toStringLong());85 // legacy Ed25519 authentication key = sha3-256(public key || 0x00); the address is that key86 expect(aptosAddressOf(pub)).toBe(AuthenticationKey.fromPublicKey({ publicKey: new Ed25519PublicKey(pub) }).derivedAddress().toStringLong());87 expect(aptosAddressOf(pub)).toBe('0x' + toHex(sha3_256(Uint8Array.from([...pub, 0]))));88 }89 expect(Account.fromDerivationPath({ path: aptosPlugin.path(net), mnemonic: MNEMONIC, legacy: true }).accountAddress.toString()).toBe(VECTOR.address);90 expect(() => aptosAddressOf(new Uint8Array(33))).toThrow();91 });92 it('accepts only the canonical long form and normalizes hexadecimal case', () => {93 expect(normalizeAddress('aptos', TO)).toBe(TO);94 expect(normalizeAddress('aptos', ` ${TO} `)).toBe(TO);95 expect(normalizeAddress('aptos', '0x' + TO.slice(2).toUpperCase())).toBe(TO);96 expect(toHex(aptosDecodeAddress(TO))).toBe(TO.slice(2));97 const bad = [98 TO.slice(2), TO.slice(0, -1), TO + '0', '0X' + TO.slice(2), TO.slice(0, -1) + 'g', '0x1', '0xa', '0x' + '0'.repeat(64), '0x' + '0'.repeat(63) + '1', '0x' + '0'.repeat(63) + 'a', '0x' + '0'.repeat(63) + 'f',99 '0x' + TO.slice(3), 'GB3JDWCQJCWMJ3IILWIGDTQJJC5567PGVEVXSCVPEQOTDN64VJBDQBYX', '0x9858EfFD232B4033E47d90003D41EC34EcaEda94', '', `${TO}\n0x`,100 ];101 for (const a of bad) expect(isValidAddress('aptos', a), a).toBe(false);102 expect(isValidAddress('aptos', '0x' + '0'.repeat(62) + '10')).toBe(true); // first address after the reserved range103 expect(() => aptosDecodeAddress('0x' + TO.slice(2).toUpperCase())).toThrow(/lowercase/); // the builder itself never normalizes104 for (const a of [TO, FROM, THIRD]) expect(AccountAddress.fromStringStrict(a).toStringLong()).toBe(a);105 });106 it('derives the primary APT store address like the framework (observed on mainnet)', () => {107 // transaction 0xbdcc8c2b…d8c9 (version 7994900991): deposit event names this store for this recipient108 expect(aptosAptStore('0xab764b960a745488b4a0fbf01f4237b7f1601332a15ef163300e9d6ff705d0db')).toBe('0xbdec6261bc2da1fd1755a9b34fd32e0e277e5d5ecf3773421c4f93f6b46fd2aa');109 });110});111112describe('aptos: transaction bytes against @aptos-labs/ts-sdk', () => {113 const variants: Intent[] = [114 base,115 withX({ sequence_number: '0', max_gas_amount: '200', gas_unit_price: '150' }, { amount: '1' }),116 withX({ sequence_number: '18446744073709551615', max_gas_amount: '2000000', gas_unit_price: '100' }, { amount: '18446744073709551615', maxFee: '200000000', to: THIRD }),117 withX({ sequence_number: '1579867', max_gas_amount: '1', gas_unit_price: '1000000' }, { amount: '8800000000', from: TO, to: FROM }),118 ];119 it('the raw transaction and signing message are byte-identical to the SDK and decode to the same fields', () => {120 for (const i of variants) {121 const b = buildAptosTx(i, net), ref = sdkRaw(i);122 expect(toHex(b.rawTx)).toBe(toHex(ref.bcsToBytes()));123 expect(toHex(b.signingMessage)).toBe(toHex(generateSigningMessageForTransaction(new SimpleTransaction(ref))));124 expect(toHex(b.signingMessage.subarray(0, 32))).toBe(toHex(sha3_256(new TextEncoder().encode('APTOS::RawTransaction'))));125 expect(aptosPlugin.build(i, net).signDocs.map(toHex)).toEqual([toHex(b.signingMessage)]);126 // decoded by the SDK: every field is the approved one, and re-encoding gives the same bytes127 const d = new Deserializer(b.rawTx), tx = RawTransaction.deserialize(d);128 expect(d.remaining()).toBe(0);129 expect(toHex(tx.bcsToBytes())).toBe(toHex(b.rawTx));130 const x = i.x!;131 expect([tx.sender.toStringLong(), tx.sequence_number, tx.max_gas_amount, tx.gas_unit_price, tx.expiration_timestamp_secs, tx.chain_id.chainId])132 .toEqual([i.from, BigInt(x.sequence_number!), BigInt(x.max_gas_amount!), BigInt(x.gas_unit_price!), BigInt(x.expiration_timestamp_secs!), 1]);133 const ef = (tx.payload as TransactionPayloadEntryFunction).entryFunction;134 expect(tx.payload).toBeInstanceOf(TransactionPayloadEntryFunction);135 expect([ef.module_name.address.toString(), ef.module_name.name.identifier, ef.function_name.identifier, ef.type_args.length, ef.args.length]).toEqual(['0x1', 'aptos_account', 'transfer', 0, 2]);136 expect(toHex(ef.args[0]!.bcsToBytes())).toBe(i.to.slice(2)); // BCS address137 expect(toHex(ef.args[1]!.bcsToBytes())).toBe(toHex(new U64(BigInt(i.amount)).bcsToBytes())); // BCS u64, little-endian138 expect(b.maxFee).toBe(BigInt(x.max_gas_amount!) * BigInt(x.gas_unit_price!));139 expect(b.describe).toMatchObject({ from: i.from, to: i.to, amountOctas: i.amount, chainId: '1', maxFeeOctas: b.maxFee.toString() });140 }141 });142 it('property: any valid field combination encodes like the SDK', () => {143 const u64 = fc.bigInt({ min: 0n, max: (1n << 64n) - 1n });144 fc.assert(fc.property(u64, fc.bigInt({ min: 1n, max: (1n << 64n) - 1n }), fc.bigInt({ min: 1n, max: 2_000_000n }), fc.bigInt({ min: 100n, max: 1_000_000n }), fc.integer({ min: 120, max: 900 }), fc.uint8Array({ minLength: 32, maxLength: 32 }),145 (seq, amount, gas, price, life, dest) => {146 const to = '0x' + toHex(dest);147 fc.pre(to !== FROM && dest.subarray(0, 31).some((v) => v !== 0));148 const i = withX({ sequence_number: seq.toString(), max_gas_amount: gas.toString(), gas_unit_price: price.toString(), expiration_timestamp_secs: String(now + life) }, { amount: amount.toString(), to, maxFee: (gas * price).toString() });149 expect(toHex(buildAptosTx(i, net).rawTx)).toBe(toHex(sdkRaw(i).bcsToBytes()));150 }), { numRuns: 300 });151 });152});153154describe('aptos: signing', () => {155 it('signs to the exact bytes the SDK produces; the SDK decodes and verifies the signature; the hash follows the Aptos rule', () => {156 for (const i of [base, withX({ sequence_number: '0' }, { amount: '1', to: THIRD })]) {157 const signed = aptosPlugin.sign(i, net, key), bytes = aptosRawBytes(signed.raw), b = buildAptosTx(i, net);158 expect(signed.raw).toMatch(/^0x[0-9a-f]+$/);159 // the SDK signing the SDK-built transaction with the same key gives the same bytes (ed25519 is deterministic)160 const transaction = new SimpleTransaction(sdkRaw(i)), account = sdkAccount();161 expect(account.accountAddress.toStringLong()).toBe(FROM);162 const senderAuthenticator = account.signTransactionWithAuthenticator(transaction);163 expect(toHex(bytes)).toBe(toHex(generateSignedTransaction({ transaction, senderAuthenticator })));164 expect(signed.txid).toBe(generateUserTransactionHash({ transaction, senderAuthenticator }));165 expect(signed.txid).toBe(aptosTxid(bytes));166 expect(signed.txid).toBe('0x' + toHex(sha3_256(Uint8Array.from([...sha3_256(new TextEncoder().encode('APTOS::Transaction')), 0, ...bytes]))));167 // decoded by the SDK: an Ed25519 authenticator carrying our key and a signature the SDK verifies over the signing message168 const d = new Deserializer(bytes), st = SignedTransaction.deserialize(d);169 expect(d.remaining()).toBe(0);170 expect(toHex(st.raw_txn.bcsToBytes())).toBe(toHex(b.rawTx));171 expect(st.authenticator).toBeInstanceOf(TransactionAuthenticatorEd25519);172 const auth = st.authenticator as TransactionAuthenticatorEd25519;173 expect(toHex(auth.public_key.toUint8Array())).toBe(toHex(PUB));174 expect(auth.public_key.verifySignature({ message: b.signingMessage, signature: auth.signature })).toBe(true);175 expect(auth.public_key.verifySignature({ message: generateSigningMessageForTransaction(transaction), signature: new Ed25519Signature(auth.signature.toUint8Array()) })).toBe(true);176 expect(ed25519.verify(auth.signature.toUint8Array(), b.signingMessage, PUB)).toBe(true);177 expect(auth.public_key.verifySignature({ message: buildAptosTx({ ...i, amount: '2' }, net).signingMessage, signature: auth.signature })).toBe(false);178 expect(toHex(aptosSignedTx(b.rawTx, PUB, auth.signature.toUint8Array()))).toBe(toHex(bytes));179 }180 });181 it('builds the simulation payloads like the SDK (zeroed signature, or no authenticator when the key is unknown)', async () => {182 const transaction = new SimpleTransaction(sdkRaw(base)), raw = buildAptosTx(base, net).rawTx;183 const zeroed = aptosSimulationTx(raw, PUB);184 expect(toHex(zeroed)).toBe(toHex(await generateSignedTransactionForSimulation({ transaction, signerPublicKey: new Ed25519PublicKey(PUB) })));185 expect(toHex(zeroed.subarray(zeroed.length - 64))).toBe('00'.repeat(64));186 expect(ed25519.verify(zeroed.subarray(zeroed.length - 64), buildAptosTx(base, net).signingMessage, PUB)).toBe(false); // can never be submitted187 expect(toHex(aptosSimulationTx(raw))).toBe(toHex(await generateSignedTransactionForSimulation({ transaction })));188 expect(toHex(aptosSimulationTx(raw))).toBe(toHex(raw) + '0404');189 });190 it('refuses a key that does not control the source, the wrong curve, and a bad signature', () => {191 const other = slip10(SEED, "m/44'/637'/1'/0'/0'");192 expect(() => aptosPlugin.sign(base, net, { curve: 'ed25519', publicKey: ed25519.getPublicKey(other), signMessage: (m) => ed25519.sign(m, other) })).toThrow(/does not control/);193 expect(() => aptosPlugin.sign(base, net, { curve: 'secp256k1', publicKey: new Uint8Array(33), signDigest: () => { throw new Error('no'); } })).toThrow(/ed25519/);194 expect(() => aptosPlugin.sign(base, net, { curve: 'ed25519', publicKey: PUB })).toThrow(/ed25519/);195 expect(() => aptosPlugin.sign(base, net, { curve: 'ed25519', publicKey: PUB, signMessage: (m) => ed25519.sign(m, other) })).toThrow(/self-check/);196 expect(() => aptosRawBytes('0x00')).toThrow();197 expect(() => aptosRawBytes(aptosPlugin.sign(base, net, key).raw.slice(2))).toThrow();198 });199});200201describe('aptos: fail closed', () => {202 const bad: [string, Intent, RegExp][] = [203 ['testnet chain id in the intent', { ...base, chainRef: '2' }, /identity/],204 ['another network id', { ...base, network: 'solana' }, /not an Aptos intent/],205 ['token asset', { ...base, asset: '0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b' }, /native/],206 ['coin type as asset', { ...base, asset: '0x1::aptos_coin::AptosCoin' }, /native/],207 ['wrong symbol', { ...base, symbol: 'USDC' }, /metadata/],208 ['wrong decimals', { ...base, decimals: 6 }, /metadata/],209 ['amount zero', { ...base, amount: '0' }, /positive/],210 ['amount above u64', { ...base, amount: '18446744073709551616' }, /range/],211 ['amount not canonical', { ...base, amount: '0125' }, /base-unit/],212 ['amount negative', { ...base, amount: '-5' }, /base-unit/],213 ['destination equals source', { ...base, to: FROM }, /equals source/],214 ['upper-case destination', { ...base, to: '0x' + TO.slice(2).toUpperCase() }, /invalid Aptos address/],215 ['destination without leading zeros restored', { ...base, to: '0x' + TO.slice(3) }, /invalid Aptos address/],216 ['destination without 0x', { ...base, to: TO.slice(2) }, /invalid Aptos address/],217 ['framework address as destination', { ...base, to: '0x' + '0'.repeat(63) + '1' }, /reserved/],218 ['APT metadata object as destination', { ...base, to: '0x' + '0'.repeat(63) + 'a' }, /reserved/],219 ['invalid source', { ...base, from: FROM.slice(0, -1) + 'z' }, /invalid Aptos address/],220 ['reserved source', { ...base, from: '0x' + '0'.repeat(64) }, /reserved/],221 ['memo', { ...base, memo: 'hello' }, /no memo/],222 ['fee above maxFee', withX({ max_gas_amount: '10001' }), /above approved maximum/],223 ['fee above maxFee through the price', withX({ gas_unit_price: '124' }), /above approved maximum/],224 ['gas price below the network minimum', withX({ gas_unit_price: '99' }), /range/],225 ['gas price zero', withX({ gas_unit_price: '0' }), /range/],226 ['gas price above u64', withX({ gas_unit_price: '18446744073709551616' }, { maxFee: '9'.repeat(30) }), /range/],227 ['gas limit zero', withX({ max_gas_amount: '0' }), /range/],228 ['gas limit above the network maximum', withX({ max_gas_amount: '2000001' }, { maxFee: '300000000' }), /range/],229 ['gas limit not canonical', withX({ max_gas_amount: '08122' }), /canonical/],230 ['gas limit negative', withX({ max_gas_amount: '-1' }), /canonical/],231 ['sequence above u64', withX({ sequence_number: '18446744073709551616' }), /range/],232 ['sequence not canonical', withX({ sequence_number: '0x81' }), /canonical/],233 ['sequence empty', withX({ sequence_number: '' }), /canonical/],234 ['expiration zero', withX({ expiration_timestamp_secs: '0' }), /range/],235 ['expiration in the past', withX({ expiration_timestamp_secs: String(now) }), /not after/],236 ['expiration before the approval window ends', withX({ expiration_timestamp_secs: String(now + 119) }), /before the approval window/],237 ['expiration too far away', withX({ expiration_timestamp_secs: String(now + 901) }), /too far/],238 ['unknown x key', withX({ payload: '00' }), /unknown or missing/],239 ['another function', withX({ function: '0x1::coin::transfer' }), /unknown or missing/],240 ['chain id override', withX({ chain_id: '1' }), /unknown or missing/],241 ['missing x key', { ...base, x: { sequence_number: '1', max_gas_amount: '200', gas_unit_price: '100' } }, /unknown or missing/],242 ['renamed x key', { ...base, x: { sequenceNumber: '1', max_gas_amount: '200', gas_unit_price: '100', expiration_timestamp_secs: String(now + 180) } }, /unknown or missing/],243 ['no x section', (() => { const { x: _x, ...r } = base; return r as Intent; })(), /missing Aptos parameters/],244 ['foreign chain section', { ...base, evm: { nonce: 0, gasLimit: '21000', maxFeePerGas: '1', maxPriorityFeePerGas: '1' } }, /unexpected chain section/],245 ['unsupported intent version', { ...base, v: 2 as never }, /version/],246 ];247 for (const [name, i, re] of bad) it(name, () => {248 expect(() => aptosPlugin.build(i, net)).toThrow(re);249 expect(() => aptosPlugin.sign(i, net, key)).toThrow(re);250 });251 it('refuses a network definition that is not the pinned main network', () => {252 expect(() => aptosPlugin.build({ ...base, chainRef: '2' }, { ...net, chainRef: '2' })).toThrow(/main network/);253 expect(() => aptosPlugin.build(base, { ...net, family: 'evm' })).toThrow(/not an Aptos intent/);254 expect(() => aptosPlugin.build({ ...base, network: 'aptos2' }, { ...net, id: 'aptos2' })).not.toThrow(); // the id is only a registry key; the chain id is what is signed255 expect(() => aptosPlugin.build({ ...base, symbol: 'ABC' }, { ...net, native: { ...net.native, symbol: 'ABC', id: 'ABC' } })).toThrow(/native/);256 expect(() => aptosPlugin.build({ ...base, decimals: 6 }, { ...net, native: { ...net.native, decimals: 6 } })).toThrow(/metadata/);257 });258 it('the intent schema and hash accept the Aptos section and bind every field', () => {259 expect(() => canonicalBytes(base)).not.toThrow();260 expect(() => canonicalBytes((() => { const { x: _x, ...r } = base; return r as Intent; })())).toThrow(/does not match network/);261 expect(() => canonicalBytes({ ...base, x: { ...base.x!, 'bad key': '1' } })).toThrow();262 });263 it('changing any intent field changes the signed bytes or throws', () => {264 const ref = toHex(aptosPlugin.build(base, net).signDocs[0]!);265 const txMuts: Intent[] = [266 { ...base, amount: '125000001' }, { ...base, to: THIRD }, { ...base, from: THIRD },267 withX({ sequence_number: '130' }), withX({ max_gas_amount: '8123' }), withX({ gas_unit_price: '101' }), withX({ expiration_timestamp_secs: String(now + 181) }),268 ];269 const seen = new Set([ref]);270 for (const m of txMuts) seen.add(toHex(aptosPlugin.build(m, net).signDocs[0]!));271 expect(seen.size).toBe(txMuts.length + 1);272 const signedRef = aptosPlugin.sign(base, net, key);273 for (const m of txMuts.filter((t) => t.from === FROM)) {274 const s = aptosPlugin.sign(m, net, key);275 expect(s.raw).not.toBe(signedRef.raw);276 expect(s.txid).not.toBe(signedRef.txid);277 }278 // fields that are not part of the Aptos transaction either throw or leave it unchanged, and always change the approved intent hash279 const other: Intent[] = [280 { ...base, chainRef: '2' }, { ...base, network: 'solana' }, { ...base, asset: 'other' }, { ...base, symbol: 'APTT' }, { ...base, decimals: 9 },281 { ...base, maxFee: '812199' }, { ...base, createdAt: now + 200 }, { ...base, expiresAt: now + 181 }, { ...base, v: 2 as never }, { ...base, memo: '1' },282 { ...base, maxFee: '1000001' }, { ...base, proposalId: '6f1c1f0e-8f5f-4b1e-9c56-0d6b5f0f1a12' }, { ...base, keyId: 'spw-0123456789abcdee' }, { ...base, policyVersion: 4 },283 ];284 const h0 = toHex(intentHash(base));285 other.forEach((m, k) => {286 let out: string | null = null;287 try { out = toHex(aptosPlugin.build(m, net).signDocs[0]!); } catch { /* refused */ }288 if (k < 10) expect(out, `mutation ${k}`).toBeNull(); else expect(out).toBe(ref);289 let h: string | null = null;290 try { h = toHex(intentHash(m)); } catch { /* refused by the schema */ }291 expect(h).not.toBe(h0);292 });293 for (const m of txMuts) expect(toHex(intentHash(m))).not.toBe(h0);294 });295});296