Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/tools/polymarket/withdraw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,8 +346,14 @@ export async function withdrawFunds(input: WithdrawInput): Promise<ToolResult> {
const account = getPolymarketAccount();
const wallet = createWalletClient({ account, chain: polygon, transport: http(POLYGON_WRITE_RPC_URL) });
// franklin-local: hash the locally signed bytes and persist before any broadcast.
const nonce = await getPublicClient().getTransactionCount({ address: account.address, blockTag: "pending" });
const request = await wallet.prepareTransactionRequest({ to: PUSD_COLLATERAL as Hex, data, chain: polygon, account, nonce });
// franklin-local: the nonce comes from the WRITE endpoint
// (prepareTransactionRequest asks the wallet's own transport), not the
// fallback public readers, which are separate providers with their own
// pending pools. A load-balanced write URL can still answer from a
// different backend than the broadcast; safety does not rest on this —
// the bytes are persisted first and a retry re-sends only those.
const request = await wallet.prepareTransactionRequest({ to: PUSD_COLLATERAL as Hex, data, chain: polygon, account });
const nonce = request.nonce;
const serializedTransaction = await wallet.signTransaction(request);
txHash = keccak256(serializedTransaction);
saveState({ pendingWithdraw: { txHash, nonce, from: account.address, serializedTransaction } });
Expand Down
9 changes: 5 additions & 4 deletions test/withdraw.local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ beforeEach(() => {
calls: [],
publicClient: {
readContract: async ({ address }) => { h.calls.push('balance'); return address === PUSD_COLLATERAL ? 10_000_000n : 0n; },
getTransactionCount: async args => { h.calls.push('nonce'); assert.equal(args.blockTag, 'pending'); return 7; },
getTransactionCount: async () => { h.calls.push('reader-nonce'); return 3; },
getTransactionReceipt: async () => { h.calls.push('receipt'); throw new Error('receipt unavailable'); },
waitForTransactionReceipt: async () => ({ status: 'success' }),
},
wallet: {
prepareTransactionRequest: async request => { h.calls.push('prepare'); assert.equal(request.nonce, 7); return request; },
prepareTransactionRequest: async request => { h.calls.push('prepare'); assert.equal(request.nonce, undefined, 'the write transport fills the nonce'); return { ...request, nonce: 7 }; },
signTransaction: async request => { h.calls.push('sign'); assert.equal(request.nonce, 7); assert.equal(loadState().pendingWithdraw, undefined); return signed; },
sendRawTransaction: async ({ serializedTransaction }) => {
h.calls.push('broadcast');
Expand Down Expand Up @@ -200,7 +200,8 @@ test('EOA persists hash, nonce and sender before raw broadcast and clears on rec
process.env.POLYMARKET_SIG_TYPE = '0';
const result = await withdrawFunds({ amount_usd: 2, confirm: true });
assert.ok(!result.isError, result.text);
assert.deepEqual(h.calls.slice(-4), ['nonce', 'prepare', 'sign', 'broadcast']);
assert.deepEqual(h.calls.slice(-3), ['prepare', 'sign', 'broadcast']);
assert.ok(!h.calls.includes('reader-nonce'), 'the nonce never comes from the public reader');
assert.equal(result.structured.transactionHash, txHash);
assert.equal(loadState().pendingWithdraw, undefined);
});
Expand Down Expand Up @@ -257,7 +258,7 @@ test('EOA guard never infers "dropped" from an advanced nonce plus an unknown ha
});

test('a retry that resolves the earlier withdrawal ends there and signs nothing new', async () => {
const fresh = ['nonce', 'prepare', 'sign', 'broadcast', 'bridge', 'submit', 'batch-sign', 'balance'];
const fresh = ['reader-nonce', 'prepare', 'sign', 'broadcast', 'bridge', 'submit', 'batch-sign', 'balance'];
const cases = [
['eoa success', () => { saveState({ pendingWithdraw: { txHash, nonce: 7, from: agent, serializedTransaction: signed } });
h.publicClient.getTransactionReceipt = async () => ({ status: 'success' }); }, /SETTLED/],
Expand Down
Loading