Skip to content

Commit af584f6

Browse files
F-OBrienclaude
andcommitted
fix: do not require a pending instruction the receiver auto-affirms
controllerTransfer builds a single leg instruction where the counter party is only receiving. Receiver affirmation is automatic unless the receiver opts in through settlement.setMandatoryReceiverAffirmation, and nothing here does, so the instruction settles on submission and never appears in the counter party's pending list. Waiting longer for it could not have helped. Check the instruction's status first and only go looking for it as pending when it has not already gone through, matching how __tests__/sdk/assets/manageNft.ts already handles the same behaviour. This reverts the poll widening from 39453d2, which was based on a wrong reading of the failure. Its sibling in sdk/settlements/tradeAssets.ts passes with no retry at all because its counter party sends on the second leg, and a sender must always affirm. The unused nonFungibleAssetControllerTransfer in the same file had the same assumption, so it gets the same treatment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 39453d2 commit af584f6

1 file changed

Lines changed: 41 additions & 26 deletions

File tree

tests/src/sdk/assets/controllerTransfer.ts

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { BigNumber, Polymesh } from '@polymeshassociation/polymesh-sdk';
2-
import { KnownNftType, MetadataType, VenueType } from '@polymeshassociation/polymesh-sdk/types';
2+
import {
3+
InstructionStatus,
4+
KnownNftType,
5+
MetadataType,
6+
VenueType,
7+
} from '@polymeshassociation/polymesh-sdk/types';
38
import assert from 'node:assert';
49

510
import { createAsset } from '~/sdk/assets/createAsset';
@@ -48,27 +53,31 @@ export const fungibleAssetControllerTransfer = async (
4853

4954
await awaitMiddlewareSynced(transferTx, sdk, 30, 3000);
5055

51-
// affirm instruction
56+
// Affirm the instruction, if it still needs affirming.
5257
//
53-
// Blocks are 6s, so the old 10 x 2s window only covered about three of them.
54-
// The counter party's pending list is served by the middleware, which lags
55-
// the chain further under load, and on a busy CI runner three blocks was not
56-
// reliably enough. 30 attempts covers ten blocks, still far inside the ~300s
57-
// (50 block) validity `getPendingInstructionEndBlock` gives the instruction.
58-
let counterInstruction;
59-
for (let attempt = 0; attempt < 30; attempt++) {
60-
const { pending } = await counterParty.getInstructions();
61-
counterInstruction = pending.find(({ id }) => id.eq(instruction.id));
62-
if (counterInstruction) {
63-
break;
58+
// A receiver's affirmation is automatic unless they have opted in via
59+
// settlement.setMandatoryReceiverAffirmation, so this single leg instruction,
60+
// where the counter party is only receiving, settles on submission and never
61+
// appears in their pending list. Only reach for the pending instruction when
62+
// it has not already gone through.
63+
const { status } = await instruction.details();
64+
65+
if (status !== InstructionStatus.Success) {
66+
let counterInstruction;
67+
for (let attempt = 0; attempt < 10; attempt++) {
68+
const { pending } = await counterParty.getInstructions();
69+
counterInstruction = pending.find(({ id }) => id.eq(instruction.id));
70+
if (counterInstruction) {
71+
break;
72+
}
73+
await sleep(2000);
6474
}
65-
await sleep(2000);
66-
}
67-
assert(counterInstruction, 'the counter party should have the instruction as pending');
75+
assert(counterInstruction, 'the counter party should have the instruction as pending');
6876

69-
const affirmTx = await counterInstruction.affirm({}, { signingAccount: counterPartyAccount });
70-
await affirmTx.run();
71-
assert(affirmTx.isSuccess);
77+
const affirmTx = await counterInstruction.affirm({}, { signingAccount: counterPartyAccount });
78+
await affirmTx.run();
79+
assert(affirmTx.isSuccess);
80+
}
7281

7382
const controllerTransferTx = await asset.controllerTransfer({
7483
originPortfolio: targetDid,
@@ -171,14 +180,20 @@ export const nonFungibleAssetControllerTransfer = async (
171180

172181
await awaitMiddlewareSynced(transferTx, sdk);
173182

174-
// affirm instruction
175-
const { pending } = await counterParty.getInstructions();
176-
const counterInstruction = pending.find(({ id }) => id.eq(instruction.id));
177-
assert(counterInstruction, 'the counter party should have the instruction as pending');
183+
// Affirm the instruction, if it still needs affirming. See the note in
184+
// fungibleAssetControllerTransfer: a pure receiver affirms automatically, so
185+
// the instruction may already have settled.
186+
const { status } = await instruction.details();
178187

179-
const affirmTx = await counterInstruction.affirm({}, { signingAccount: counterPartyAccount });
180-
await affirmTx.run();
181-
assert(affirmTx.isSuccess);
188+
if (status !== InstructionStatus.Success) {
189+
const { pending } = await counterParty.getInstructions();
190+
const counterInstruction = pending.find(({ id }) => id.eq(instruction.id));
191+
assert(counterInstruction, 'the counter party should have the instruction as pending');
192+
193+
const affirmTx = await counterInstruction.affirm({}, { signingAccount: counterPartyAccount });
194+
await affirmTx.run();
195+
assert(affirmTx.isSuccess);
196+
}
182197

183198
const controllerTransferTx = await collection.controllerTransfer({
184199
originPortfolio: targetDid,

0 commit comments

Comments
 (0)