Skip to content

Commit 04788e1

Browse files
authored
Merge pull request #9007 from BitGo/SCAAS-9727
feat(sdk-coin-canton): handle ISO timestamp vs microsecond mismatch in assertDeepCantonMatch
2 parents c8943b6 + b158331 commit 04788e1

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

modules/sdk-coin-canton/src/lib/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,17 @@ export class Utils implements BaseUtils {
637637
}
638638
return;
639639
}
640+
// ISO 8601 input vs microsecond-since-epoch encoding in the prepared-transaction protobuf.
641+
if (this.isIsoTimestamp(expected) && this.isIntegerString(actual)) {
642+
const expectedMicroseconds = new BigNumber(new Date(expected).getTime()).multipliedBy(1000);
643+
if (!expectedMicroseconds.isEqualTo(new BigNumber(actual))) {
644+
throw new Error(
645+
`Canton command timestamp mismatch at '${currentPath || '<root>'}': ` +
646+
`expected '${expected}' (${expectedMicroseconds.toFixed(0)} µs), got '${actual}'`
647+
);
648+
}
649+
return;
650+
}
640651
if (expected !== actual) {
641652
throw new Error(
642653
`Canton command mismatch at '${currentPath || '<root>'}': expected '${expected}', got '${actual}'`
@@ -854,6 +865,17 @@ export class Utils implements BaseUtils {
854865
return numericRe.test(a) && numericRe.test(b);
855866
}
856867

868+
private isIsoTimestamp(value: string): boolean {
869+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,3})?Z$/.test(value)) {
870+
return false;
871+
}
872+
return Number.isFinite(Date.parse(value));
873+
}
874+
875+
private isIntegerString(value: string): boolean {
876+
return /^\d+$/.test(value);
877+
}
878+
857879
private describeType(value: unknown): string {
858880
if (value === null) return 'null';
859881
if (Array.isArray(value)) return 'array';

modules/sdk-coin-canton/test/unit/utils.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,5 +508,65 @@ describe('Canton Utils - CantonCommand helpers', function () {
508508
it('should throw on type mismatch', function () {
509509
assert.throws(() => utils.assertDeepCantonMatch({ a: 'str' }, { a: 42 }, noInject), /mismatch/);
510510
});
511+
512+
describe('ISO timestamp vs microsecond string normalisation', function () {
513+
const isoTs = '2026-06-12T10:00:00.000Z';
514+
const isoTsUs = String(new Date(isoTs).getTime() * 1000);
515+
516+
it('should pass when ISO timestamp (expected) equals microseconds (actual)', function () {
517+
assert.doesNotThrow(() => utils.assertDeepCantonMatch(isoTs, isoTsUs, noInject));
518+
});
519+
520+
it('should throw timestamp mismatch when microseconds do not correspond to the ISO value', function () {
521+
const wrongUs = String(new Date(isoTs).getTime() * 1000 + 1_000_000);
522+
assert.throws(() => utils.assertDeepCantonMatch(isoTs, wrongUs, noInject), /timestamp mismatch/);
523+
});
524+
525+
it('should pass for ISO timestamp nested inside a mint-like choiceArgument object', function () {
526+
const executeBefore = '2026-06-13T10:00:00.000Z';
527+
const executeBeforeUs = String(new Date(executeBefore).getTime() * 1000);
528+
529+
const expected = {
530+
expectedAdmin: 'registrar::1220abc',
531+
mint: {
532+
instrumentId: { admin: 'registrar::1220abc', id: 'STGUSD1' },
533+
amount: '1000000.0',
534+
holder: 'registrar::1220abc',
535+
reference: 'mint-stgusd1-12345',
536+
requestedAt: isoTs,
537+
executeBefore: executeBefore,
538+
meta: { values: {} },
539+
},
540+
};
541+
542+
const actual = {
543+
expectedAdmin: 'registrar::1220abc',
544+
mint: {
545+
instrumentId: { admin: 'registrar::1220abc', id: 'STGUSD1' },
546+
amount: '1000000.0000000000',
547+
holder: 'registrar::1220abc',
548+
reference: 'mint-stgusd1-12345',
549+
requestedAt: isoTsUs,
550+
executeBefore: executeBeforeUs,
551+
meta: { values: {} },
552+
},
553+
};
554+
555+
assert.doesNotThrow(() => utils.assertDeepCantonMatch(expected, actual, noInject));
556+
});
557+
558+
it('should throw when ISO timestamp in nested object does not match microseconds', function () {
559+
const wrongUs = String(new Date(isoTs).getTime() * 1000 + 5_000_000);
560+
561+
const expected = { mint: { requestedAt: isoTs } };
562+
const actual = { mint: { requestedAt: wrongUs } };
563+
564+
assert.throws(() => utils.assertDeepCantonMatch(expected, actual, noInject), /timestamp mismatch/);
565+
});
566+
567+
it('should not treat an arbitrary non-timestamp ISO-like string as a timestamp', function () {
568+
assert.throws(() => utils.assertDeepCantonMatch('not-a-timestamp', isoTsUs, noInject), /mismatch/);
569+
});
570+
});
511571
});
512572
});

0 commit comments

Comments
 (0)