ox 0.14.25 → 0.14.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import type * as Address from '../core/Address.js'
3
3
  import type * as Errors from '../core/Errors.js'
4
4
  import * as Hash from '../core/Hash.js'
5
5
  import * as Hex from '../core/Hex.js'
6
- import type { Compute } from '../core/internal/types.js'
6
+ import type { Compute, OneOf } from '../core/internal/types.js'
7
7
  import * as Rlp from '../core/Rlp.js'
8
8
  import * as SignatureEnvelope from './SignatureEnvelope.js'
9
9
  import * as TempoAddress from './TempoAddress.js'
@@ -55,13 +55,38 @@ export type KeyAuthorization<
55
55
  scopes?: readonly Scope<addressType>[] | undefined
56
56
  /** Key type. (secp256k1, P256, WebAuthn). */
57
57
  type: SignatureEnvelope.Type
58
- } & (signed extends true
59
- ? { signature: SignatureEnvelope.SignatureEnvelope<bigintType, numberType> }
60
- : {
61
- signature?:
62
- | SignatureEnvelope.SignatureEnvelope<bigintType, numberType>
63
- | undefined
64
- })
58
+ /**
59
+ * Optional 32-byte witness bound into the signing hash.
60
+ *
61
+ * Applications use this to bind a single signature to an arbitrary offchain
62
+ * context (e.g. a server-issued challenge), or as a revocation handle that
63
+ * can be burned onchain to invalidate the authorization before submission.
64
+ *
65
+ * [TIP-1053 Specification](https://tips.sh/1053)
66
+ */
67
+ witness?: Hex.Hex | undefined
68
+ } & OneOf<
69
+ // TIP-1049 admin access keys: `account` and `isAdmin` are paired — either
70
+ // both are specified or neither. The `account` binding scopes the signing
71
+ // hash to a specific account, and `isAdmin: true` provisions an admin
72
+ // access key with unrestricted keychain mutator privileges.
73
+ //
74
+ // [TIP-1049 Specification](https://tips.sh/1049)
75
+ | {
76
+ /** Account address this authorization is bound to. */
77
+ account: addressType
78
+ /** Whether this authorization provisions an admin access key. */
79
+ isAdmin: boolean
80
+ }
81
+ | {}
82
+ > &
83
+ (signed extends true
84
+ ? { signature: SignatureEnvelope.SignatureEnvelope<bigintType, numberType> }
85
+ : {
86
+ signature?:
87
+ | SignatureEnvelope.SignatureEnvelope<bigintType, numberType>
88
+ | undefined
89
+ })
65
90
 
66
91
  /** Input type for a Key Authorization. */
67
92
  export type Input = KeyAuthorization<
@@ -73,12 +98,16 @@ export type Input = KeyAuthorization<
73
98
 
74
99
  /** RPC representation matching the node's wire format. */
75
100
  export type Rpc = {
101
+ /** Optional account address binding (TIP-1049). */
102
+ account?: Address.Address | null | undefined
76
103
  /** Allowed call scopes (node field: `allowedCalls`). */
77
104
  allowedCalls?: readonly RpcCallScope[] | undefined
78
105
  /** Chain ID (hex quantity). */
79
106
  chainId: Hex.Hex
80
107
  /** Expiry timestamp (hex quantity or null). */
81
108
  expiry: Hex.Hex | null | undefined
109
+ /** Whether this authorization provisions an admin access key (TIP-1049). */
110
+ isAdmin?: boolean | null | undefined
82
111
  /** Key identifier. */
83
112
  keyId: Address.Address
84
113
  /** Key type. */
@@ -87,6 +116,8 @@ export type Rpc = {
87
116
  limits?: readonly RpcTokenLimit[] | undefined
88
117
  /** Signature envelope. */
89
118
  signature: SignatureEnvelope.SignatureEnvelopeRpc
119
+ /** Optional 32-byte witness (hex). */
120
+ witness?: Hex.Hex | null | undefined
90
121
  }
91
122
 
92
123
  /** RPC representation of a token limit (matches node's `TokenLimit` serde). */
@@ -145,6 +176,30 @@ type AuthorizationTuple =
145
176
  limits: readonly TokenLimitTuple[],
146
177
  calls: readonly CallScopeTuple[],
147
178
  ]
179
+ | readonly [
180
+ ...BaseTuple,
181
+ expiry: Hex.Hex,
182
+ limits: readonly TokenLimitTuple[],
183
+ calls: readonly CallScopeTuple[],
184
+ witness: Hex.Hex,
185
+ ]
186
+ | readonly [
187
+ ...BaseTuple,
188
+ expiry: Hex.Hex,
189
+ limits: readonly TokenLimitTuple[],
190
+ calls: readonly CallScopeTuple[],
191
+ witness: Hex.Hex,
192
+ isAdmin: Hex.Hex,
193
+ ]
194
+ | readonly [
195
+ ...BaseTuple,
196
+ expiry: Hex.Hex,
197
+ limits: readonly TokenLimitTuple[],
198
+ calls: readonly CallScopeTuple[],
199
+ witness: Hex.Hex,
200
+ isAdmin: Hex.Hex,
201
+ account: Address.Address,
202
+ ]
148
203
 
149
204
  /** Tuple representation of a Key Authorization. */
150
205
  export type Tuple<signed extends boolean = boolean> = signed extends true
@@ -362,6 +417,7 @@ export function from<
362
417
  recipients?: readonly TempoAddress.Address[]
363
418
  }[]
364
419
  }
420
+ if (auth.witness !== undefined) assertWitness(auth.witness)
365
421
  const resolved = {
366
422
  ...auth,
367
423
  address: TempoAddress.resolve(auth.address as TempoAddress.Address),
@@ -455,7 +511,11 @@ export declare namespace from {
455
511
  export function fromRpc(authorization: Rpc): Signed {
456
512
  const { allowedCalls, chainId, keyId, expiry, limits, keyType } =
457
513
  authorization
514
+ const witness = authorization.witness ?? undefined
515
+ const isAdmin = authorization.isAdmin ?? undefined
516
+ const account = authorization.account ?? undefined
458
517
  const signature = SignatureEnvelope.fromRpc(authorization.signature)
518
+ if (witness !== undefined) assertWitness(witness)
459
519
 
460
520
  // Unflatten nested allowedCalls into flat scopes
461
521
  const scopes = allowedCalls
@@ -488,6 +548,9 @@ export function fromRpc(authorization: Rpc): Signed {
488
548
  ...(scopes ? { scopes } : {}),
489
549
  signature,
490
550
  type: keyType,
551
+ ...(witness !== undefined ? { witness } : {}),
552
+ ...(isAdmin ? { isAdmin: true } : {}),
553
+ ...(account !== undefined ? { account } : {}),
491
554
  }
492
555
  }
493
556
 
@@ -538,7 +601,13 @@ export function fromTuple<const tuple extends Tuple>(
538
601
  tuple: tuple,
539
602
  ): fromTuple.ReturnType<tuple> {
540
603
  const [authorization, signatureSerialized] = tuple
541
- const [chainId, keyType_hex, keyId, expiry, limits, scopes] = authorization
604
+ const [chainId, keyType_hex, keyId, ...trailing] =
605
+ authorization as unknown as [
606
+ Hex.Hex,
607
+ Hex.Hex,
608
+ Address.Address,
609
+ ...unknown[],
610
+ ]
542
611
  const keyType = (() => {
543
612
  switch (keyType_hex) {
544
613
  case '0x':
@@ -552,54 +621,71 @@ export function fromTuple<const tuple extends Tuple>(
552
621
  throw new Error(`Invalid key type: ${keyType_hex}`)
553
622
  }
554
623
  })()
624
+ // Trailing optional fields in wire order. Each entry pulls one slot off the
625
+ // trailing array and decodes it (treating absent or RLP-null placeholders as
626
+ // missing). To add a new optional trailing field, append a single entry.
627
+ const [rawExpiry, rawLimits, rawScopes, rawWitness, rawIsAdmin, rawAccount] =
628
+ trailing
629
+ const expiry = isAbsent(rawExpiry)
630
+ ? undefined
631
+ : hexToNumber(rawExpiry as Hex.Hex) || undefined
632
+ const limits =
633
+ Array.isArray(rawLimits) && rawLimits.length > 0
634
+ ? rawLimits.map((limitTuple: any) => {
635
+ const [token, limit, period] = limitTuple
636
+ return {
637
+ token,
638
+ limit: hexToBigint(limit),
639
+ ...(period !== undefined ? { period: hexToNumber(period) } : {}),
640
+ }
641
+ })
642
+ : undefined
643
+ const scopes = Array.isArray(rawScopes)
644
+ ? rawScopes.flatMap((scopeTuple: any) => {
645
+ const [address, selectorRules] = scopeTuple
646
+ // If no selector rules, this is an address-only scope.
647
+ if (!Array.isArray(selectorRules) || selectorRules.length === 0)
648
+ return [{ address }]
649
+ // Flatten each selector rule into a separate scope entry.
650
+ return selectorRules.map((ruleTuple: any) => {
651
+ const [selector, recipients] = ruleTuple
652
+ return {
653
+ address,
654
+ selector,
655
+ ...(Array.isArray(recipients) && recipients.length > 0
656
+ ? { recipients }
657
+ : {}),
658
+ }
659
+ })
660
+ })
661
+ : undefined
662
+ const witness = isAbsent(rawWitness) ? undefined : (rawWitness as Hex.Hex)
663
+ if (witness !== undefined) assertWitness(witness)
664
+ const isAdmin = (() => {
665
+ if (isAbsent(rawIsAdmin)) return undefined
666
+ // TIP-1049: the admin marker is strictly `0x01`. Any other value is a
667
+ // protocol-level decode error on the node, so reject it here too.
668
+ if (rawIsAdmin !== '0x01')
669
+ throw new InvalidAdminMarkerError(rawIsAdmin as Hex.Hex)
670
+ return true
671
+ })()
672
+ const account = isAbsent(rawAccount)
673
+ ? undefined
674
+ : (rawAccount as Address.Address)
675
+ // TIP-1049 admin fields are paired: only emit both when both are present on
676
+ // the wire. Wire shapes carrying only one are tolerated for forward-compat
677
+ // but the orphan field is dropped (since the public API requires both).
678
+ const adminPair =
679
+ account !== undefined && isAdmin ? { account, isAdmin: true as const } : {}
555
680
  const args: KeyAuthorization = {
556
681
  address: keyId,
557
- expiry:
558
- typeof expiry !== 'undefined'
559
- ? hexToNumber(expiry) || undefined
560
- : undefined,
561
- type: keyType,
562
682
  chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
563
- ...(typeof expiry !== 'undefined'
564
- ? { expiry: hexToNumber(expiry) || undefined }
565
- : {}),
566
- ...(typeof limits !== 'undefined' &&
567
- Array.isArray(limits) &&
568
- limits.length > 0
569
- ? {
570
- limits: limits.map((limitTuple: any) => {
571
- const [token, limit, period] = limitTuple
572
- return {
573
- token,
574
- limit: hexToBigint(limit),
575
- ...(typeof period !== 'undefined'
576
- ? { period: hexToNumber(period) }
577
- : {}),
578
- }
579
- }),
580
- }
581
- : {}),
582
- ...(typeof scopes !== 'undefined' && Array.isArray(scopes)
583
- ? {
584
- scopes: scopes.flatMap((scopeTuple: any) => {
585
- const [address, selectorRules] = scopeTuple
586
- // If no selector rules, this is an address-only scope
587
- if (!Array.isArray(selectorRules) || selectorRules.length === 0)
588
- return [{ address }]
589
- // Flatten each selector rule into a separate scope entry
590
- return selectorRules.map((ruleTuple: any) => {
591
- const [selector, recipients] = ruleTuple
592
- return {
593
- address,
594
- selector,
595
- ...(Array.isArray(recipients) && recipients.length > 0
596
- ? { recipients }
597
- : {}),
598
- }
599
- })
600
- }),
601
- }
602
- : {}),
683
+ type: keyType,
684
+ ...(expiry !== undefined ? { expiry } : {}),
685
+ ...(limits !== undefined ? { limits } : {}),
686
+ ...(scopes !== undefined ? { scopes } : {}),
687
+ ...(witness !== undefined ? { witness } : {}),
688
+ ...adminPair,
603
689
  }
604
690
  if (signatureSerialized)
605
691
  args.signature = SignatureEnvelope.deserialize(signatureSerialized)
@@ -803,8 +889,19 @@ export declare namespace serialize {
803
889
  * @returns An RPC-formatted Key Authorization.
804
890
  */
805
891
  export function toRpc(authorization: Signed): Rpc {
806
- const { address, scopes, chainId, expiry, limits, type, signature } =
807
- authorization
892
+ const {
893
+ address,
894
+ scopes,
895
+ chainId,
896
+ expiry,
897
+ limits,
898
+ type,
899
+ signature,
900
+ witness,
901
+ isAdmin,
902
+ account,
903
+ } = authorization
904
+ if (witness !== undefined) assertWitness(witness)
808
905
 
809
906
  // Group flat scopes by address into nested allowedCalls wire format
810
907
  const allowedCalls = (() => {
@@ -842,6 +939,9 @@ export function toRpc(authorization: Signed): Rpc {
842
939
  })),
843
940
  signature: SignatureEnvelope.toRpc(signature),
844
941
  ...(allowedCalls ? { allowedCalls } : {}),
942
+ ...(witness !== undefined ? { witness } : {}),
943
+ ...(isAdmin ? { isAdmin: true } : {}),
944
+ ...(account !== undefined ? { account } : {}),
845
945
  }
846
946
  }
847
947
 
@@ -883,7 +983,17 @@ export declare namespace toRpc {
883
983
  export function toTuple<const authorization extends KeyAuthorization>(
884
984
  authorization: authorization,
885
985
  ): toTuple.ReturnType<authorization> {
886
- const { address, chainId, scopes, expiry, limits } = authorization
986
+ const {
987
+ address,
988
+ chainId,
989
+ scopes,
990
+ expiry,
991
+ limits,
992
+ witness,
993
+ isAdmin,
994
+ account,
995
+ } = authorization
996
+ if (witness !== undefined) assertWitness(witness)
887
997
  const signature = authorization.signature
888
998
  ? SignatureEnvelope.serialize(authorization.signature)
889
999
  : undefined
@@ -930,21 +1040,51 @@ export function toTuple<const authorization extends KeyAuthorization>(
930
1040
  selectorRules.map(([selector, recipients]) => [selector, recipients]),
931
1041
  ])
932
1042
  })()
933
- const authorizationTuple = [
934
- bigintToHex(chainId),
935
- type,
936
- address,
937
- // expiry is required in the tuple when limits or scopes are present
938
- // expiry=0 is treated the same as undefined (never expires)
939
- (expiry !== null && expiry !== undefined && expiry !== 0) ||
940
- limitsValue ||
941
- callsValue
942
- ? numberToHex(expiry ?? 0)
943
- : undefined,
944
- // limits is required in the tuple when scopes are present
945
- limitsValue || callsValue ? (limitsValue ?? []) : undefined,
946
- callsValue,
947
- ].filter((x) => typeof x !== 'undefined')
1043
+ // Optional trailing fields in wire order. Each entry's `placeholder` is
1044
+ // emitted when this field is skipped but a later field is present.
1045
+ //
1046
+ // Placeholder convention:
1047
+ // - `'0x'` (RLP null) is the canonical placeholder for fields added at or
1048
+ // after TIP-1053. The node decodes it as `None` (unrestricted).
1049
+ // - `limits` keeps `[]` as its skipped placeholder for pre-TIP-1053 wire
1050
+ // shapes — preserving byte-for-byte equivalence with signed payloads
1051
+ // produced before TIP-1053 was added. When any TIP-1053+ field is
1052
+ // present, the canonical `'0x'` placeholder is used instead.
1053
+ //
1054
+ // To add a new optional trailing field (e.g. from a future TIP): append a
1055
+ // single entry to this list with `placeholder: '0x'`.
1056
+ const hasTip1053Plus =
1057
+ witness !== undefined || isAdmin || account !== undefined
1058
+ const optionals: readonly { value: unknown; placeholder: unknown }[] = [
1059
+ {
1060
+ value:
1061
+ expiry !== null && expiry !== undefined && expiry !== 0
1062
+ ? numberToHex(expiry)
1063
+ : undefined,
1064
+ placeholder: '0x',
1065
+ },
1066
+ {
1067
+ value: limitsValue,
1068
+ placeholder: hasTip1053Plus ? '0x' : [],
1069
+ },
1070
+ { value: callsValue, placeholder: '0x' },
1071
+ { value: witness, placeholder: '0x' },
1072
+ // TIP-1049: admin marker. Present = `0x01` (RLP integer 1); absent
1073
+ // skipped or omitted. Any other value is a hard decode error on the node.
1074
+ { value: isAdmin ? '0x01' : undefined, placeholder: '0x' },
1075
+ // TIP-1049: optional account binding. Last field — never a placeholder.
1076
+ { value: account, placeholder: '0x' },
1077
+ ]
1078
+ let lastPresent = -1
1079
+ for (let i = optionals.length - 1; i >= 0; i--)
1080
+ if (optionals[i]!.value !== undefined) {
1081
+ lastPresent = i
1082
+ break
1083
+ }
1084
+ const trailing = optionals
1085
+ .slice(0, lastPresent + 1)
1086
+ .map(({ value, placeholder }) => value ?? placeholder)
1087
+ const authorizationTuple = [bigintToHex(chainId), type, address, ...trailing]
948
1088
  return [authorizationTuple, ...(signature ? [signature] : [])] as never
949
1089
  }
950
1090
 
@@ -985,3 +1125,31 @@ function resolveSelector(
985
1125
  if (selector.startsWith('0x')) return selector as Hex.Hex
986
1126
  return AbiItem.getSelector(selector)
987
1127
  }
1128
+
1129
+ function assertWitness(witness: Hex.Hex): void {
1130
+ if (Hex.size(witness) !== 32) throw new InvalidWitnessSizeError(witness)
1131
+ }
1132
+
1133
+ function isAbsent(value: unknown): boolean {
1134
+ return value === undefined || value === '0x'
1135
+ }
1136
+
1137
+ /** Thrown when a `witness` field is not exactly 32 bytes. */
1138
+ export class InvalidWitnessSizeError extends Error {
1139
+ override readonly name = 'KeyAuthorization.InvalidWitnessSizeError'
1140
+ constructor(witness: Hex.Hex) {
1141
+ super(
1142
+ `Witness \`${witness}\` must be exactly 32 bytes (got ${Hex.size(witness)} bytes).`,
1143
+ )
1144
+ }
1145
+ }
1146
+
1147
+ /** Thrown when a TIP-1049 admin marker has any value other than `0x01`. */
1148
+ export class InvalidAdminMarkerError extends Error {
1149
+ override readonly name = 'KeyAuthorization.InvalidAdminMarkerError'
1150
+ constructor(marker: Hex.Hex) {
1151
+ super(
1152
+ `Admin marker \`${marker}\` is invalid; expected \`0x01\` (TIP-1049).`,
1153
+ )
1154
+ }
1155
+ }