ox 0.14.28 → 0.14.29

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.
@@ -1097,6 +1097,80 @@ describe('from', () => {
1097
1097
  expect(envelope.keyId).toBe(explicitKeyId)
1098
1098
  })
1099
1099
  })
1100
+
1101
+ describe('multisig', () => {
1102
+ const genesisConfig = MultisigConfig.from({
1103
+ threshold: 1,
1104
+ owners: [
1105
+ {
1106
+ owner: '0x1111111111111111111111111111111111111111',
1107
+ weight: 1,
1108
+ },
1109
+ ],
1110
+ })
1111
+
1112
+ test('behavior: derives `account`/`genesisConfigId` from `genesisConfig`', () => {
1113
+ const envelope = SignatureEnvelope.from({
1114
+ genesisConfig,
1115
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1116
+ })
1117
+
1118
+ expect(envelope).toMatchObject({
1119
+ type: 'multisig',
1120
+ account: MultisigConfig.getAddress(genesisConfig),
1121
+ genesisConfigId: MultisigConfig.toId(genesisConfig),
1122
+ })
1123
+ expect('genesisConfig' in envelope).toBe(false)
1124
+ expect((envelope as SignatureEnvelope.Multisig).init).toBeUndefined()
1125
+ })
1126
+
1127
+ test('behavior: `init: true` opts into bootstrap (uses `genesisConfig` as `init`)', () => {
1128
+ const envelope = SignatureEnvelope.from({
1129
+ genesisConfig,
1130
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1131
+ init: true,
1132
+ })
1133
+
1134
+ expect((envelope as SignatureEnvelope.Multisig).init).toEqual(
1135
+ genesisConfig,
1136
+ )
1137
+ })
1138
+
1139
+ test('behavior: `init` accepts an explicit config', () => {
1140
+ const otherConfig = MultisigConfig.from({
1141
+ threshold: 1,
1142
+ owners: [
1143
+ {
1144
+ owner: '0x2222222222222222222222222222222222222222',
1145
+ weight: 1,
1146
+ },
1147
+ ],
1148
+ })
1149
+ const envelope = SignatureEnvelope.from({
1150
+ genesisConfig,
1151
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1152
+ init: otherConfig,
1153
+ })
1154
+
1155
+ expect((envelope as SignatureEnvelope.Multisig).init).toEqual(otherConfig)
1156
+ })
1157
+
1158
+ test('behavior: `{account, genesisConfigId}` form still works', () => {
1159
+ const account = MultisigConfig.getAddress(genesisConfig)
1160
+ const genesisConfigId = MultisigConfig.toId(genesisConfig)
1161
+ const envelope = SignatureEnvelope.from({
1162
+ account,
1163
+ genesisConfigId,
1164
+ signatures: [SignatureEnvelope.from(signature_secp256k1)],
1165
+ })
1166
+
1167
+ expect(envelope).toMatchObject({
1168
+ type: 'multisig',
1169
+ account,
1170
+ genesisConfigId,
1171
+ })
1172
+ })
1173
+ })
1100
1174
  })
1101
1175
 
1102
1176
  describe('getType', () => {
@@ -1698,33 +1772,40 @@ describe('serialize', () => {
1698
1772
  })
1699
1773
 
1700
1774
  describe('sortMultisigApprovals', () => {
1701
- const account = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const
1702
- const configId = `0x${'11'.repeat(32)}` as const
1703
- const payload = `0x${'42'.repeat(32)}` as const
1704
- const digest = MultisigConfig.getSignPayload({
1705
- account,
1706
- configId,
1707
- payload,
1708
- })
1709
-
1710
- const owners = Array.from({ length: 3 }, () => {
1775
+ // Build owner key pairs first so we can construct a real genesis config
1776
+ // whose owner set matches the keys that produce the approvals below.
1777
+ const ownerKeys = Array.from({ length: 3 }, () => {
1711
1778
  const privateKey = Secp256k1.randomPrivateKey()
1712
1779
  const address = Address.fromPublicKey(
1713
1780
  Secp256k1.getPublicKey({ privateKey }),
1714
1781
  )
1715
- const signature = SignatureEnvelope.from(
1716
- Secp256k1.sign({ payload: digest, privateKey }),
1717
- )
1718
- return { address, signature } as const
1782
+ return { address, privateKey } as const
1719
1783
  })
1720
- const ascending = [...owners].sort((a, b) =>
1784
+ const ascendingOwners = [...ownerKeys].sort((a, b) =>
1721
1785
  Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
1722
1786
  )
1723
1787
 
1788
+ const genesisConfig = MultisigConfig.from({
1789
+ threshold: 2,
1790
+ owners: ascendingOwners.map((o) => ({ owner: o.address, weight: 1 })),
1791
+ })
1792
+ const payload = `0x${'42'.repeat(32)}` as const
1793
+ const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
1794
+
1795
+ const owners = ownerKeys.map((owner) => ({
1796
+ address: owner.address,
1797
+ signature: SignatureEnvelope.from(
1798
+ Secp256k1.sign({ payload: digest, privateKey: owner.privateKey }),
1799
+ ),
1800
+ }))
1801
+ const ascending = ascendingOwners.map((o) => ({
1802
+ address: o.address,
1803
+ signature: owners.find((x) => x.address === o.address)!.signature,
1804
+ }))
1805
+
1724
1806
  test('behavior: orders approvals ascending by recovered owner address', () => {
1725
1807
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1726
- account,
1727
- configId,
1808
+ genesisConfig,
1728
1809
  payload,
1729
1810
  // Provide approvals in reverse of the canonical order.
1730
1811
  signatures: [...ascending].reverse().map((owner) => owner.signature),
@@ -1736,8 +1817,7 @@ describe('sortMultisigApprovals', () => {
1736
1817
  const signatures = ascending.map((owner) => owner.signature)
1737
1818
  expect(
1738
1819
  SignatureEnvelope.sortMultisigApprovals({
1739
- account,
1740
- configId,
1820
+ genesisConfig,
1741
1821
  payload,
1742
1822
  signatures,
1743
1823
  }),
@@ -1746,8 +1826,7 @@ describe('sortMultisigApprovals', () => {
1746
1826
 
1747
1827
  test('behavior: recovered order matches the config owner order', () => {
1748
1828
  const ordered = SignatureEnvelope.sortMultisigApprovals({
1749
- account,
1750
- configId,
1829
+ genesisConfig,
1751
1830
  payload,
1752
1831
  signatures: owners.map((owner) => owner.signature),
1753
1832
  })
@@ -1756,6 +1835,25 @@ describe('sortMultisigApprovals', () => {
1756
1835
  )
1757
1836
  expect(recovered).toEqual(ascending.map((owner) => owner.address))
1758
1837
  })
1838
+
1839
+ test('behavior: `genesisConfig` and `{account, genesisConfigId}` produce identical ordering', () => {
1840
+ const account = MultisigConfig.getAddress(genesisConfig)
1841
+ const genesisConfigId = MultisigConfig.toId(genesisConfig)
1842
+ const signatures = owners.map((owner) => owner.signature)
1843
+
1844
+ const fromConfig = SignatureEnvelope.sortMultisigApprovals({
1845
+ genesisConfig,
1846
+ payload,
1847
+ signatures,
1848
+ })
1849
+ const fromIds = SignatureEnvelope.sortMultisigApprovals({
1850
+ account,
1851
+ genesisConfigId,
1852
+ payload,
1853
+ signatures,
1854
+ })
1855
+ expect(fromConfig).toEqual(fromIds)
1856
+ })
1759
1857
  })
1760
1858
 
1761
1859
  describe('validate', () => {
@@ -2757,7 +2855,7 @@ describe('CoercionError', () => {
2757
2855
 
2758
2856
  describe('multisig', () => {
2759
2857
  const account = '0x8ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7ae'
2760
- const configId =
2858
+ const genesisConfigId =
2761
2859
  '0x01781fe551182476f2422c759e82d81c92e3263737afbbad57def6e8b69d21f5'
2762
2860
 
2763
2861
  // P256 signatures do not carry `yParity` in the wire format, so use a clean
@@ -2771,7 +2869,7 @@ describe('multisig', () => {
2771
2869
  const envelope = SignatureEnvelope.from({
2772
2870
  type: 'multisig',
2773
2871
  account,
2774
- configId,
2872
+ genesisConfigId,
2775
2873
  signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2776
2874
  })
2777
2875
 
@@ -2825,7 +2923,7 @@ describe('multisig', () => {
2825
2923
  const bootstrapEnvelope = SignatureEnvelope.from({
2826
2924
  type: 'multisig',
2827
2925
  account,
2828
- configId,
2926
+ genesisConfigId,
2829
2927
  signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2830
2928
  init,
2831
2929
  })
@@ -2841,7 +2939,7 @@ describe('multisig', () => {
2841
2939
  const salted = SignatureEnvelope.from({
2842
2940
  type: 'multisig',
2843
2941
  account,
2844
- configId,
2942
+ genesisConfigId,
2845
2943
  signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
2846
2944
  init: { ...init, salt: `0x${'42'.repeat(32)}` },
2847
2945
  })
@@ -2895,7 +2993,7 @@ describe('multisig', () => {
2895
2993
  SignatureEnvelope.assert({
2896
2994
  type: 'multisig',
2897
2995
  account,
2898
- configId,
2996
+ genesisConfigId,
2899
2997
  signatures: [],
2900
2998
  init: { threshold: 1, owners: [] },
2901
2999
  } as never),
@@ -74,7 +74,7 @@ export type GetType<
74
74
  ? 'keychain'
75
75
  : envelope extends {
76
76
  account: Address.Address
77
- configId: `0x${string}`
77
+ genesisConfigId: `0x${string}`
78
78
  signatures: any
79
79
  }
80
80
  ? 'multisig'
@@ -161,7 +161,7 @@ export type Multisig<bigintType = bigint, numberType = number> = {
161
161
  /** Native multisig account address. */
162
162
  account: Address.Address
163
163
  /** Permanent config ID derived from the initial multisig config. */
164
- configId: Hex.Hex
164
+ genesisConfigId: Hex.Hex
165
165
  /** Primitive owner approvals over the multisig owner approval digest. */
166
166
  signatures: readonly SignatureEnvelope<bigintType, numberType>[]
167
167
  /**
@@ -175,6 +175,11 @@ export type Multisig<bigintType = bigint, numberType = number> = {
175
175
  export type MultisigRpc = {
176
176
  type: 'multisig'
177
177
  account: Address.Address
178
+ /**
179
+ * The permanent multisig config ID (TIP-1061 wire field `config_id`).
180
+ * Maps to `genesisConfigId` on the typed
181
+ * {@link ox#SignatureEnvelope.Multisig} envelope.
182
+ */
178
183
  configId: Hex.Hex
179
184
  /**
180
185
  * Encoded primitive owner approvals (raw serialized signatures), matching the
@@ -328,7 +333,7 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
328
333
  const multisig = envelope as Multisig
329
334
  const missing: string[] = []
330
335
  if (!multisig.account) missing.push('account')
331
- if (!multisig.configId) missing.push('configId')
336
+ if (!multisig.genesisConfigId) missing.push('genesisConfigId')
332
337
  if (!Array.isArray(multisig.signatures)) missing.push('signatures')
333
338
  if (missing.length > 0)
334
339
  throw new MissingPropertiesError({ envelope, missing, type: 'multisig' })
@@ -613,10 +618,10 @@ export function deserialize(value: Serialized): SignatureEnvelope {
613
618
  }
614
619
 
615
620
  if (typeId === serializedMultisigType) {
616
- // Wire format: `0x05 || rlp([account, configId, signatures, init])`. `init`
621
+ // Wire format: `0x05 || rlp([account, genesisConfigId, signatures, init])`. `init`
617
622
  // is optional: absent when the element is missing or the `0x80` placeholder
618
623
  // (decoded as the empty string `0x`), otherwise the bootstrap config list.
619
- const [account, configId, signatures, init] = Rlp.toHex(data) as [
624
+ const [account, genesisConfigId, signatures, init] = Rlp.toHex(data) as [
620
625
  Hex.Hex,
621
626
  Hex.Hex,
622
627
  readonly Hex.Hex[],
@@ -625,7 +630,7 @@ export function deserialize(value: Serialized): SignatureEnvelope {
625
630
  return {
626
631
  type: 'multisig',
627
632
  account,
628
- configId,
633
+ genesisConfigId,
629
634
  signatures: signatures.map((signature) => deserialize(signature)),
630
635
  ...(init && init !== '0x'
631
636
  ? {
@@ -753,6 +758,43 @@ export function deserialize(value: Serialized): SignatureEnvelope {
753
758
  * })
754
759
  * ```
755
760
  *
761
+ * @example
762
+ * ### Multisig (from genesis config)
763
+ *
764
+ * Pass `genesisConfig` to derive `account` and `genesisConfigId` automatically.
765
+ * Set `init: true` to opt into bootstrap (uses `genesisConfig` as the
766
+ * bootstrap `init`); omit `init` for subsequent (non-bootstrap) transactions.
767
+ *
768
+ * ```ts twoslash
769
+ * import { Secp256k1 } from 'ox'
770
+ * import { MultisigConfig, SignatureEnvelope } from 'ox/tempo'
771
+ *
772
+ * const genesisConfig = MultisigConfig.from({
773
+ * threshold: 1,
774
+ * owners: [
775
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
776
+ * ],
777
+ * })
778
+ *
779
+ * const privateKey = Secp256k1.randomPrivateKey()
780
+ * const signature = SignatureEnvelope.from(
781
+ * Secp256k1.sign({ payload: '0xdeadbeef', privateKey }),
782
+ * )
783
+ *
784
+ * // Bootstrap transaction
785
+ * const bootstrap = SignatureEnvelope.from({
786
+ * genesisConfig,
787
+ * signatures: [signature],
788
+ * init: true,
789
+ * })
790
+ *
791
+ * // Subsequent (non-bootstrap) transactions
792
+ * const subsequent = SignatureEnvelope.from({
793
+ * genesisConfig,
794
+ * signatures: [signature],
795
+ * })
796
+ * ```
797
+ *
756
798
  * @param value - The value to coerce (either a hex string or signature envelope).
757
799
  * @returns The signature envelope.
758
800
  */
@@ -774,20 +816,42 @@ export function from<const value extends from.Value>(
774
816
  const type = getType(value)
775
817
 
776
818
  if (type === 'multisig') {
777
- const multisig = value as Multisig
819
+ const multisig = value as Multisig & {
820
+ genesisConfig?: MultisigConfig.Config | undefined
821
+ init?: MultisigConfig.Config | boolean | undefined
822
+ }
823
+ const { genesisConfig, init, ...rest } = multisig
824
+ // Derive `account`/`genesisConfigId` from `genesisConfig` when not provided
825
+ // explicitly.
826
+ const account = (() => {
827
+ if (rest.account) return rest.account
828
+ if (genesisConfig) return MultisigConfig.getAddress(genesisConfig)
829
+ return rest.account
830
+ })()
831
+ const genesisConfigId = (() => {
832
+ if (rest.genesisConfigId) return rest.genesisConfigId
833
+ if (genesisConfig) return MultisigConfig.toId(genesisConfig)
834
+ return rest.genesisConfigId
835
+ })()
836
+ // `init: true` opts into bootstrap using the supplied `genesisConfig`.
837
+ // Otherwise, `init` is treated as the explicit bootstrap config (or
838
+ // omitted).
839
+ const initSource = init === true ? genesisConfig : init || undefined
778
840
  return {
779
- ...multisig,
780
- signatures: multisig.signatures.map((signature) => from(signature)),
841
+ ...rest,
842
+ account,
843
+ genesisConfigId,
844
+ signatures: rest.signatures.map((signature) => from(signature)),
781
845
  // Normalize the bootstrap config (sorts owners, defaults the salt) so the
782
846
  // in-memory envelope matches what `deserialize` reconstructs.
783
- ...(multisig.init ? { init: MultisigConfig.from(multisig.init) } : {}),
847
+ ...(initSource ? { init: MultisigConfig.from(initSource) } : {}),
784
848
  type,
785
849
  } as never
786
850
  }
787
851
 
788
852
  return {
789
853
  ...value,
790
- ...(type === 'p256' ? { prehash: value.prehash } : {}),
854
+ ...(type === 'p256' ? { prehash: (value as P256).prehash } : {}),
791
855
  ...(type === 'keychain'
792
856
  ? {
793
857
  ...(!(
@@ -827,10 +891,24 @@ export declare namespace from {
827
891
  payload?: Hex.Hex | Bytes.Bytes | undefined
828
892
  }
829
893
 
894
+ /**
895
+ * Multisig envelope input variant where `account` and `genesisConfigId` are derived
896
+ * from the supplied `genesisConfig`. Pass `init: true` to opt into bootstrap
897
+ * (uses `genesisConfig` as the bootstrap `init`); omit `init` for subsequent
898
+ * (non-bootstrap) transactions.
899
+ */
900
+ type MultisigFromGenesisConfig = {
901
+ type?: 'multisig' | undefined
902
+ genesisConfig: MultisigConfig.Config
903
+ signatures: readonly SignatureEnvelope[]
904
+ init?: MultisigConfig.Config | boolean | undefined
905
+ }
906
+
830
907
  type Value =
831
908
  | UnionPartialBy<SignatureEnvelope, 'prehash' | 'type'>
832
909
  | Secp256k1Flat
833
910
  | Serialized
911
+ | MultisigFromGenesisConfig
834
912
 
835
913
  type ReturnValue<value extends Value> = Compute<
836
914
  OneOf<
@@ -838,16 +916,18 @@ export declare namespace from {
838
916
  ? SignatureEnvelope
839
917
  : value extends Secp256k1Flat
840
918
  ? Secp256k1
841
- : IsNarrowable<value, SignatureEnvelope> extends true
842
- ? SignatureEnvelope
843
- : Assign<
844
- value,
845
- {
846
- readonly type: GetType<value>
847
- } & (GetType<value> extends 'keychain'
848
- ? { keyId?: Address.Address | undefined }
849
- : {})
850
- >
919
+ : value extends MultisigFromGenesisConfig
920
+ ? Multisig
921
+ : IsNarrowable<value, SignatureEnvelope> extends true
922
+ ? SignatureEnvelope
923
+ : Assign<
924
+ value,
925
+ {
926
+ readonly type: GetType<value>
927
+ } & (GetType<value> extends 'keychain'
928
+ ? { keyId?: Address.Address | undefined }
929
+ : {})
930
+ >
851
931
  >
852
932
  >
853
933
  }
@@ -963,7 +1043,9 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
963
1043
  return {
964
1044
  type: 'multisig',
965
1045
  account: multisig.account,
966
- configId: multisig.configId,
1046
+ // Map RPC wire field `configId` (TIP-1061 spec name) to the typed
1047
+ // envelope's `genesisConfigId`.
1048
+ genesisConfigId: multisig.configId,
967
1049
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
968
1050
  signatures: multisig.signatures.map((signature) =>
969
1051
  deserialize(signature),
@@ -1050,8 +1132,8 @@ export function getType<
1050
1132
 
1051
1133
  // Detect Multisig signature
1052
1134
  if (
1053
- 'account' in envelope &&
1054
- 'configId' in envelope &&
1135
+ (('account' in envelope && 'genesisConfigId' in envelope) ||
1136
+ 'genesisConfig' in envelope) &&
1055
1137
  'signatures' in envelope
1056
1138
  )
1057
1139
  return 'multisig' as never
@@ -1151,7 +1233,7 @@ export function serialize(
1151
1233
 
1152
1234
  if (type === 'multisig') {
1153
1235
  const multisig = envelope as Multisig
1154
- // Format: `0x05 || rlp([account, configId, signatures, init])`, where each
1236
+ // Format: `0x05 || rlp([account, genesisConfigId, signatures, init])`, where each
1155
1237
  // owner approval is an encoded primitive signature. `init` is the bootstrap
1156
1238
  // config (an RLP list) when present, otherwise the canonical empty-string
1157
1239
  // placeholder (`0x` → RLP `0x80`).
@@ -1159,7 +1241,7 @@ export function serialize(
1159
1241
  serializedMultisigType,
1160
1242
  Rlp.fromHex([
1161
1243
  multisig.account,
1162
- multisig.configId,
1244
+ multisig.genesisConfigId,
1163
1245
  multisig.signatures.map((signature) => serialize(signature)),
1164
1246
  multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
1165
1247
  ]),
@@ -1191,33 +1273,33 @@ export declare namespace serialize {
1191
1273
  * recovered owner address. Works for any owner key type (secp256k1, p256,
1192
1274
  * webAuthn, keychain).
1193
1275
  *
1276
+ * Config updates never change `account`/`genesisConfigId`, so the genesis
1277
+ * config is the correct input even for post-update transactions.
1278
+ *
1194
1279
  * @example
1195
1280
  * ```ts twoslash
1196
1281
  * import { Secp256k1 } from 'ox'
1197
1282
  * import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
1198
1283
  *
1199
- * const config = MultisigConfig.from({
1284
+ * const genesisConfig = MultisigConfig.from({
1200
1285
  * threshold: 2,
1201
1286
  * owners: [
1202
1287
  * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
1203
1288
  * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
1204
1289
  * ],
1205
1290
  * })
1206
- * const configId = MultisigConfig.toId(config)
1207
- * const account = MultisigConfig.getAddress({ configId })
1208
1291
  *
1209
1292
  * const tx = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
1210
1293
  * const payload = TxEnvelopeTempo.getSignPayload(tx)
1211
- * const digest = MultisigConfig.getSignPayload({ payload, account, configId })
1212
1294
  *
1213
1295
  * const privateKeys = [Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey()]
1296
+ * const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
1214
1297
  * const signatures = privateKeys.map((privateKey) =>
1215
1298
  * SignatureEnvelope.from(Secp256k1.sign({ payload: digest, privateKey })),
1216
1299
  * )
1217
1300
  *
1218
1301
  * const ordered = SignatureEnvelope.sortMultisigApprovals({ // [!code focus]
1219
- * account, // [!code focus]
1220
- * configId, // [!code focus]
1302
+ * genesisConfig, // [!code focus]
1221
1303
  * payload, // [!code focus]
1222
1304
  * signatures, // [!code focus]
1223
1305
  * }) // [!code focus]
@@ -1229,12 +1311,17 @@ export declare namespace serialize {
1229
1311
  export function sortMultisigApprovals(
1230
1312
  value: sortMultisigApprovals.Value,
1231
1313
  ): readonly SignatureEnvelope[] {
1232
- const { account, configId, payload, signatures } = value
1233
- const digest = MultisigConfig.getSignPayload({
1234
- account,
1235
- configId,
1236
- payload,
1237
- })
1314
+ const { payload, signatures } = value
1315
+ const digest = MultisigConfig.getSignPayload(
1316
+ 'genesisConfig' in value && value.genesisConfig
1317
+ ? { payload, genesisConfig: value.genesisConfig }
1318
+ : {
1319
+ payload,
1320
+ account: (value as { account: Address.Address }).account,
1321
+ genesisConfigId: (value as { genesisConfigId: Hex.Hex })
1322
+ .genesisConfigId,
1323
+ },
1324
+ )
1238
1325
  // Recover each signer once (decorate–sort–undecorate) rather than inside the
1239
1326
  // comparator.
1240
1327
  return signatures
@@ -1248,15 +1335,26 @@ export function sortMultisigApprovals(
1248
1335
 
1249
1336
  export declare namespace sortMultisigApprovals {
1250
1337
  type Value = {
1251
- /** The native multisig account address. */
1252
- account: Address.Address
1253
- /** The permanent config ID. */
1254
- configId: Hex.Hex
1255
1338
  /** The inner transaction sign payload (`tx.signature_hash()`). */
1256
1339
  payload: Hex.Hex | Bytes.Bytes
1257
1340
  /** The primitive owner approvals to order. */
1258
1341
  signatures: readonly SignatureEnvelope[]
1259
- }
1342
+ } & OneOf<
1343
+ | {
1344
+ /** The native multisig account address. */
1345
+ account: Address.Address
1346
+ /** The permanent config ID. */
1347
+ genesisConfigId: Hex.Hex
1348
+ }
1349
+ | {
1350
+ /**
1351
+ * The initial multisig config (the bootstrap config that derived the
1352
+ * permanent `account` and `genesisConfigId`). Used to derive both values
1353
+ * automatically.
1354
+ */
1355
+ genesisConfig: MultisigConfig.Config
1356
+ }
1357
+ >
1260
1358
 
1261
1359
  type ErrorType =
1262
1360
  | MultisigConfig.getSignPayload.ErrorType
@@ -1336,7 +1434,9 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1336
1434
  return {
1337
1435
  type: 'multisig',
1338
1436
  account: multisig.account,
1339
- configId: multisig.configId,
1437
+ // Map the typed envelope's `genesisConfigId` to the RPC wire field
1438
+ // `configId` (TIP-1061 spec name).
1439
+ configId: multisig.genesisConfigId,
1340
1440
  // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1341
1441
  signatures: multisig.signatures.map((signature) => serialize(signature)),
1342
1442
  ...(multisig.init ? { init: multisig.init } : {}),