ox 0.14.27 → 0.14.28

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.
@@ -0,0 +1,312 @@
1
+ import * as Address from '../core/Address.js';
2
+ import * as Errors from '../core/Errors.js';
3
+ import * as Hash from '../core/Hash.js';
4
+ import * as Hex from '../core/Hex.js';
5
+ /** Maximum number of owners allowed in a native multisig config. */
6
+ export const maxOwners = 10;
7
+ /** Maximum encoded byte length for one primitive owner approval. */
8
+ export const maxOwnerSignatureBytes = 2049;
9
+ /** Tempo signature type byte for native multisig signatures. */
10
+ export const signatureTypeByte = '0x05';
11
+ /** Zero 32-byte salt (the default when no salt is provided). */
12
+ export const zeroSalt = `0x${'00'.repeat(32)}`;
13
+ /** Domain prefix for the native multisig account address derivation. */
14
+ const accountDomain = 'tempo:multisig:account';
15
+ /** Domain prefix for the native multisig config ID derivation. */
16
+ const configDomain = 'tempo:multisig:config';
17
+ /** Domain prefix for native multisig owner approvals. */
18
+ const signatureDomain = 'tempo:multisig:signature';
19
+ /**
20
+ * Asserts that a native multisig {@link ox#MultisigConfig.Config} is valid.
21
+ *
22
+ * Mirrors the Tempo `validate_multisig_config` rules: owners non-empty and
23
+ * `<= maxOwners`, strictly ascending unique nonzero owner addresses, nonzero
24
+ * owner weights, `threshold >= 1`, total weight `<= u32::MAX`, and
25
+ * `threshold <= total weight`.
26
+ *
27
+ * @example
28
+ * ```ts twoslash
29
+ * import { MultisigConfig } from 'ox/tempo'
30
+ *
31
+ * MultisigConfig.assert({
32
+ * threshold: 1,
33
+ * owners: [
34
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
35
+ * ],
36
+ * })
37
+ * ```
38
+ *
39
+ * @param config - The multisig config.
40
+ */
41
+ export function assert(config) {
42
+ const { salt, threshold, owners } = config;
43
+ if (typeof salt !== 'undefined' && Hex.size(salt) !== 32)
44
+ throw new InvalidConfigError({ reason: 'salt must be 32 bytes' });
45
+ if (owners.length === 0)
46
+ throw new InvalidConfigError({ reason: 'owners cannot be empty' });
47
+ if (owners.length > maxOwners)
48
+ throw new InvalidConfigError({ reason: 'too many owners' });
49
+ if (Number(threshold) < 1)
50
+ throw new InvalidConfigError({ reason: 'threshold cannot be zero' });
51
+ let totalWeight = 0;
52
+ let previous;
53
+ for (const owner of owners) {
54
+ if (!Address.validate(owner.owner) || Hex.toBigInt(owner.owner) === 0n)
55
+ throw new InvalidConfigError({ reason: 'owner cannot be zero' });
56
+ if (Number(owner.weight) < 1)
57
+ throw new InvalidConfigError({ reason: 'owner weight cannot be zero' });
58
+ const current = Hex.toBigInt(owner.owner);
59
+ if (typeof previous !== 'undefined' && previous >= current)
60
+ throw new InvalidConfigError({
61
+ reason: 'owners must be strictly ascending',
62
+ });
63
+ previous = current;
64
+ totalWeight += Number(owner.weight);
65
+ }
66
+ if (totalWeight > 0xffffffff)
67
+ throw new InvalidConfigError({
68
+ reason: 'total owner weight exceeds u32 max',
69
+ });
70
+ if (Number(threshold) > totalWeight)
71
+ throw new InvalidConfigError({
72
+ reason: 'threshold exceeds total owner weight',
73
+ });
74
+ }
75
+ /**
76
+ * Normalizes a native multisig {@link ox#MultisigConfig.Config}.
77
+ *
78
+ * Sorts owners into strictly ascending `owner` address order (the canonical
79
+ * form required for config ID derivation) and asserts the config is valid.
80
+ *
81
+ * @example
82
+ * ```ts twoslash
83
+ * import { MultisigConfig } from 'ox/tempo'
84
+ *
85
+ * const config = MultisigConfig.from({
86
+ * threshold: 2,
87
+ * owners: [
88
+ * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
89
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
90
+ * ],
91
+ * })
92
+ * // owners are now sorted ascending by address
93
+ * ```
94
+ *
95
+ * @param config - The multisig config.
96
+ * @returns The normalized multisig config.
97
+ */
98
+ export function from(config) {
99
+ const owners = [...config.owners].sort((a, b) => Hex.toBigInt(a.owner) < Hex.toBigInt(b.owner) ? -1 : 1);
100
+ const normalized = {
101
+ salt: config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt,
102
+ threshold: config.threshold,
103
+ owners,
104
+ };
105
+ assert(normalized);
106
+ return normalized;
107
+ }
108
+ /**
109
+ * Converts an RLP {@link ox#MultisigConfig.Tuple} back to a
110
+ * {@link ox#MultisigConfig.Config}.
111
+ *
112
+ * @example
113
+ * ```ts twoslash
114
+ * import { MultisigConfig } from 'ox/tempo'
115
+ *
116
+ * const config = MultisigConfig.fromTuple([
117
+ * `0x${'00'.repeat(32)}`,
118
+ * '0x01',
119
+ * [['0x1111111111111111111111111111111111111111', '0x01']],
120
+ * ])
121
+ * ```
122
+ *
123
+ * @param tuple - The RLP tuple.
124
+ * @returns The multisig config.
125
+ */
126
+ export function fromTuple(tuple) {
127
+ const [salt, threshold, owners] = tuple;
128
+ return {
129
+ salt: salt && salt !== '0x' ? Hex.padLeft(salt, 32) : zeroSalt,
130
+ threshold: threshold === '0x' ? 0 : Hex.toNumber(threshold),
131
+ owners: owners.map((owner) => {
132
+ const [ownerAddress, weight] = owner;
133
+ return {
134
+ owner: ownerAddress,
135
+ weight: !weight || weight === '0x' ? 0 : Hex.toNumber(weight),
136
+ };
137
+ }),
138
+ };
139
+ }
140
+ /**
141
+ * Derives the stable native multisig account address.
142
+ *
143
+ * `keccak256("tempo:multisig:account" || config_id)[12:32]`.
144
+ *
145
+ * @example
146
+ * ```ts twoslash
147
+ * import { MultisigConfig } from 'ox/tempo'
148
+ *
149
+ * const config = MultisigConfig.from({
150
+ * threshold: 1,
151
+ * owners: [
152
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
153
+ * ],
154
+ * })
155
+ *
156
+ * const address = MultisigConfig.getAddress({ config })
157
+ * ```
158
+ *
159
+ * @param value - The config or config ID to derive the address from.
160
+ * @returns The multisig account address.
161
+ */
162
+ export function getAddress(value) {
163
+ const id = 'configId' in value ? value.configId : toId(value.config);
164
+ const hash = Hash.keccak256(Hex.concat(Hex.fromString(accountDomain), id));
165
+ return Address.from(Hex.slice(hash, 12, 32));
166
+ }
167
+ /**
168
+ * Computes the digest a native multisig owner approves (signs).
169
+ *
170
+ * `keccak256("tempo:multisig:signature" || inner_digest || account || config_id)`,
171
+ * where `inner_digest` is the transaction sign payload
172
+ * ({@link ox#TxEnvelopeTempo.(getSignPayload:function)}).
173
+ *
174
+ * @example
175
+ * ```ts twoslash
176
+ * import { MultisigConfig, TxEnvelopeTempo } from 'ox/tempo'
177
+ *
178
+ * const config = MultisigConfig.from({
179
+ * threshold: 1,
180
+ * owners: [
181
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
182
+ * ],
183
+ * })
184
+ * const configId = MultisigConfig.toId(config)
185
+ * const account = MultisigConfig.getAddress({ configId })
186
+ *
187
+ * const envelope = TxEnvelopeTempo.from({
188
+ * chainId: 1,
189
+ * calls: [],
190
+ * })
191
+ *
192
+ * const digest = MultisigConfig.getSignPayload({
193
+ * payload: TxEnvelopeTempo.getSignPayload(envelope),
194
+ * account,
195
+ * configId,
196
+ * })
197
+ * ```
198
+ *
199
+ * @param value - The digest derivation parameters.
200
+ * @returns The owner approval digest.
201
+ */
202
+ export function getSignPayload(value) {
203
+ const { payload, account, configId } = value;
204
+ return Hash.keccak256(Hex.concat(Hex.fromString(signatureDomain), Hex.from(payload), account, configId));
205
+ }
206
+ /**
207
+ * Derives the permanent config ID for a native multisig
208
+ * {@link ox#MultisigConfig.Config}.
209
+ *
210
+ * Preimage (fixed-width big-endian, **not** RLP):
211
+ * `keccak256("tempo:multisig:config" || salt || be_u32(threshold) || be_u32(owners.length) || (owner || be_u32(weight)) for each owner)`.
212
+ *
213
+ * @example
214
+ * ```ts twoslash
215
+ * import { MultisigConfig } from 'ox/tempo'
216
+ *
217
+ * const config = MultisigConfig.from({
218
+ * threshold: 1,
219
+ * owners: [
220
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
221
+ * ],
222
+ * })
223
+ *
224
+ * const configId = MultisigConfig.toId(config)
225
+ * ```
226
+ *
227
+ * @param config - The multisig config.
228
+ * @returns The 32-byte config ID.
229
+ */
230
+ export function toId(config) {
231
+ assert(config);
232
+ const id = Hash.keccak256(Hex.concat(Hex.fromString(configDomain), Hex.padLeft(config.salt ?? zeroSalt, 32), Hex.fromNumber(config.threshold, { size: 4 }), Hex.fromNumber(config.owners.length, { size: 4 }), ...config.owners.flatMap((owner) => [
233
+ owner.owner,
234
+ Hex.fromNumber(owner.weight, { size: 4 }),
235
+ ])));
236
+ if (Hex.toBigInt(id) === 0n)
237
+ throw new InvalidConfigError({ reason: 'config ID cannot be zero' });
238
+ return id;
239
+ }
240
+ /**
241
+ * Converts a {@link ox#MultisigConfig.Config} to its RLP tuple form (carried
242
+ * by the multisig signature `init`).
243
+ *
244
+ * Tuple shape: `[salt, threshold, [[owner, weight], ...]]`. The
245
+ * 32-byte `salt` encodes as a full fixed-width string; other integers use
246
+ * canonical RLP encoding (zero values encode as `0x`).
247
+ *
248
+ * @example
249
+ * ```ts twoslash
250
+ * import { MultisigConfig } from 'ox/tempo'
251
+ *
252
+ * const tuple = MultisigConfig.toTuple({
253
+ * threshold: 1,
254
+ * owners: [
255
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
256
+ * ],
257
+ * })
258
+ * ```
259
+ *
260
+ * @param config - The multisig config.
261
+ * @returns The RLP tuple.
262
+ */
263
+ export function toTuple(config) {
264
+ assert(config);
265
+ const owners = config.owners.map((owner) => [owner.owner, Hex.fromNumber(owner.weight)]);
266
+ // `salt` is a fixed 32-byte value: it RLP-encodes as a full 32-byte string
267
+ // (including the zero salt), never trimmed like an integer.
268
+ const salt = config.salt ? Hex.padLeft(config.salt, 32) : zeroSalt;
269
+ return [salt, Hex.fromNumber(config.threshold), owners];
270
+ }
271
+ /**
272
+ * Validates a native multisig {@link ox#MultisigConfig.Config}. Returns `true`
273
+ * if valid, `false` otherwise.
274
+ *
275
+ * @example
276
+ * ```ts twoslash
277
+ * import { MultisigConfig } from 'ox/tempo'
278
+ *
279
+ * const valid = MultisigConfig.validate({
280
+ * threshold: 1,
281
+ * owners: [
282
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
283
+ * ],
284
+ * })
285
+ * // @log: true
286
+ * ```
287
+ *
288
+ * @param config - The multisig config.
289
+ * @returns Whether the config is valid.
290
+ */
291
+ export function validate(config) {
292
+ try {
293
+ assert(config);
294
+ return true;
295
+ }
296
+ catch {
297
+ return false;
298
+ }
299
+ }
300
+ /** Thrown when a native multisig config is invalid. */
301
+ export class InvalidConfigError extends Errors.BaseError {
302
+ constructor({ reason }) {
303
+ super(`Invalid native multisig config: ${reason}.`);
304
+ Object.defineProperty(this, "name", {
305
+ enumerable: true,
306
+ configurable: true,
307
+ writable: true,
308
+ value: 'MultisigConfig.InvalidConfigError'
309
+ });
310
+ }
311
+ }
312
+ //# sourceMappingURL=MultisigConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultisigConfig.js","sourceRoot":"","sources":["../../tempo/MultisigConfig.ts"],"names":[],"mappings":"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"}
@@ -3,14 +3,17 @@ import * as Errors from '../core/Errors.js';
3
3
  import * as Hex from '../core/Hex.js';
4
4
  import * as Json from '../core/Json.js';
5
5
  import * as ox_P256 from '../core/P256.js';
6
+ import * as Rlp from '../core/Rlp.js';
6
7
  import * as ox_Secp256k1 from '../core/Secp256k1.js';
7
8
  import * as Signature from '../core/Signature.js';
8
9
  import * as ox_WebAuthnP256 from '../core/WebAuthnP256.js';
10
+ import * as MultisigConfig from './MultisigConfig.js';
9
11
  /** Signature type identifiers for encoding/decoding */
10
12
  const serializedP256Type = '0x01';
11
13
  const serializedWebAuthnType = '0x02';
12
14
  const serializedKeychainType = '0x03';
13
15
  const serializedKeychainV2Type = '0x04';
16
+ const serializedMultisigType = '0x05';
14
17
  /** Serialized magic identifier for Tempo signature envelopes. */
15
18
  export const magicBytes = '0x7777777777777777777777777777777777777777777777777777777777777777'; // 32 "T"s
16
19
  /** List of supported signature types. */
@@ -95,6 +98,23 @@ export function assert(envelope) {
95
98
  assert(keychain.inner);
96
99
  return;
97
100
  }
101
+ if (type === 'multisig') {
102
+ const multisig = envelope;
103
+ const missing = [];
104
+ if (!multisig.account)
105
+ missing.push('account');
106
+ if (!multisig.configId)
107
+ missing.push('configId');
108
+ if (!Array.isArray(multisig.signatures))
109
+ missing.push('signatures');
110
+ if (missing.length > 0)
111
+ throw new MissingPropertiesError({ envelope, missing, type: 'multisig' });
112
+ for (const inner of multisig.signatures)
113
+ assert(inner);
114
+ if (multisig.init)
115
+ MultisigConfig.assert(multisig.init);
116
+ return;
117
+ }
98
118
  }
99
119
  /**
100
120
  * Extracts the address of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
@@ -128,6 +148,10 @@ export function extractAddress(options) {
128
148
  return signature.userAddress;
129
149
  return extractAddress({ ...options, signature: signature.inner });
130
150
  }
151
+ // Native multisig signatures have no single signer; the recovered sender is the
152
+ // derived multisig account address.
153
+ if (signature.type === 'multisig')
154
+ return signature.account;
131
155
  return Address.fromPublicKey(extractPublicKey(options));
132
156
  }
133
157
  /**
@@ -168,6 +192,10 @@ export function extractPublicKey(options) {
168
192
  return signature.publicKey;
169
193
  case 'keychain':
170
194
  return extractPublicKey({ payload, signature: signature.inner });
195
+ case 'multisig':
196
+ // A multisig signature aggregates multiple owner approvals and has no
197
+ // single public key; recover the multisig account via `extractAddress`.
198
+ throw new CoercionError({ envelope: signature });
171
199
  }
172
200
  }
173
201
  /**
@@ -290,8 +318,25 @@ export function deserialize(value) {
290
318
  version: typeId === serializedKeychainV2Type ? 'v2' : 'v1',
291
319
  };
292
320
  }
321
+ if (typeId === serializedMultisigType) {
322
+ // Wire format: `0x05 || rlp([account, configId, signatures, init])`. `init`
323
+ // is optional: absent when the element is missing or the `0x80` placeholder
324
+ // (decoded as the empty string `0x`), otherwise the bootstrap config list.
325
+ const [account, configId, signatures, init] = Rlp.toHex(data);
326
+ return {
327
+ type: 'multisig',
328
+ account,
329
+ configId,
330
+ signatures: signatures.map((signature) => deserialize(signature)),
331
+ ...(init && init !== '0x'
332
+ ? {
333
+ init: MultisigConfig.fromTuple(init),
334
+ }
335
+ : {}),
336
+ };
337
+ }
293
338
  throw new InvalidSerializedError({
294
- reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), or ${serializedKeychainV2Type} (Keychain V2)`,
339
+ reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), ${serializedKeychainV2Type} (Keychain V2), or ${serializedMultisigType} (Multisig)`,
295
340
  serialized,
296
341
  });
297
342
  }
@@ -420,6 +465,17 @@ export function from(value, options) {
420
465
  'yParity' in value)
421
466
  return { signature: value, type: 'secp256k1' };
422
467
  const type = getType(value);
468
+ if (type === 'multisig') {
469
+ const multisig = value;
470
+ return {
471
+ ...multisig,
472
+ signatures: multisig.signatures.map((signature) => from(signature)),
473
+ // Normalize the bootstrap config (sorts owners, defaults the salt) so the
474
+ // in-memory envelope matches what `deserialize` reconstructs.
475
+ ...(multisig.init ? { init: MultisigConfig.from(multisig.init) } : {}),
476
+ type,
477
+ };
478
+ }
423
479
  return {
424
480
  ...value,
425
481
  ...(type === 'p256' ? { prehash: value.prehash } : {}),
@@ -532,14 +588,30 @@ export function fromRpc(envelope) {
532
588
  };
533
589
  }
534
590
  if (envelope.type === 'keychain' ||
535
- ('userAddress' in envelope && 'signature' in envelope))
591
+ ('userAddress' in envelope && 'signature' in envelope)) {
592
+ const keychain = envelope;
536
593
  return {
537
594
  type: 'keychain',
538
- userAddress: envelope.userAddress,
539
- inner: fromRpc(envelope.signature),
540
- ...(envelope.keyId ? { keyId: envelope.keyId } : {}),
541
- ...(envelope.version ? { version: envelope.version } : {}),
595
+ userAddress: keychain.userAddress,
596
+ inner: fromRpc(keychain.signature),
597
+ ...(keychain.keyId ? { keyId: keychain.keyId } : {}),
598
+ ...(keychain.version ? { version: keychain.version } : {}),
542
599
  };
600
+ }
601
+ if (envelope.type === 'multisig' ||
602
+ ('account' in envelope &&
603
+ 'configId' in envelope &&
604
+ 'signatures' in envelope)) {
605
+ const multisig = envelope;
606
+ return {
607
+ type: 'multisig',
608
+ account: multisig.account,
609
+ configId: multisig.configId,
610
+ // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
611
+ signatures: multisig.signatures.map((signature) => deserialize(signature)),
612
+ ...(multisig.init ? { init: MultisigConfig.from(multisig.init) } : {}),
613
+ };
614
+ }
543
615
  throw new CoercionError({ envelope });
544
616
  }
545
617
  /**
@@ -590,6 +662,11 @@ export function getType(envelope) {
590
662
  // Detect Keychain signature
591
663
  if ('userAddress' in envelope && 'inner' in envelope)
592
664
  return 'keychain';
665
+ // Detect Multisig signature
666
+ if ('account' in envelope &&
667
+ 'configId' in envelope &&
668
+ 'signatures' in envelope)
669
+ return 'multisig';
593
670
  throw new CoercionError({
594
671
  envelope,
595
672
  });
@@ -645,8 +722,84 @@ export function serialize(envelope, options = {}) {
645
722
  : serializedKeychainV2Type;
646
723
  return Hex.concat(keychainTypeId, keychain.userAddress, serialize(keychain.inner), options.magic ? magicBytes : '0x');
647
724
  }
725
+ if (type === 'multisig') {
726
+ const multisig = envelope;
727
+ // Format: `0x05 || rlp([account, configId, signatures, init])`, where each
728
+ // owner approval is an encoded primitive signature. `init` is the bootstrap
729
+ // config (an RLP list) when present, otherwise the canonical empty-string
730
+ // placeholder (`0x` → RLP `0x80`).
731
+ return Hex.concat(serializedMultisigType, Rlp.fromHex([
732
+ multisig.account,
733
+ multisig.configId,
734
+ multisig.signatures.map((signature) => serialize(signature)),
735
+ multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
736
+ ]), options.magic ? magicBytes : '0x');
737
+ }
648
738
  throw new CoercionError({ envelope });
649
739
  }
740
+ /**
741
+ * Orders native multisig owner approvals into the strictly-ascending
742
+ * recovered-owner order the Tempo node requires for the multisig `signatures`
743
+ * array (the node enforces "recovered owners must be strictly ascending").
744
+ *
745
+ * Each approval is signed over the multisig owner approval digest
746
+ * ({@link ox#MultisigConfig.(getSignPayload:function)}), so the signer of
747
+ * every approval is recovered against that digest and the list is sorted by the
748
+ * recovered owner address. Works for any owner key type (secp256k1, p256,
749
+ * webAuthn, keychain).
750
+ *
751
+ * @example
752
+ * ```ts twoslash
753
+ * import { Secp256k1 } from 'ox'
754
+ * import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
755
+ *
756
+ * const config = MultisigConfig.from({
757
+ * threshold: 2,
758
+ * owners: [
759
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
760
+ * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
761
+ * ],
762
+ * })
763
+ * const configId = MultisigConfig.toId(config)
764
+ * const account = MultisigConfig.getAddress({ configId })
765
+ *
766
+ * const tx = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
767
+ * const payload = TxEnvelopeTempo.getSignPayload(tx)
768
+ * const digest = MultisigConfig.getSignPayload({ payload, account, configId })
769
+ *
770
+ * const privateKeys = [Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey()]
771
+ * const signatures = privateKeys.map((privateKey) =>
772
+ * SignatureEnvelope.from(Secp256k1.sign({ payload: digest, privateKey })),
773
+ * )
774
+ *
775
+ * const ordered = SignatureEnvelope.sortMultisigApprovals({ // [!code focus]
776
+ * account, // [!code focus]
777
+ * configId, // [!code focus]
778
+ * payload, // [!code focus]
779
+ * signatures, // [!code focus]
780
+ * }) // [!code focus]
781
+ * ```
782
+ *
783
+ * @param value - The approval ordering parameters.
784
+ * @returns The owner approvals ordered ascending by recovered owner address.
785
+ */
786
+ export function sortMultisigApprovals(value) {
787
+ const { account, configId, payload, signatures } = value;
788
+ const digest = MultisigConfig.getSignPayload({
789
+ account,
790
+ configId,
791
+ payload,
792
+ });
793
+ // Recover each signer once (decorate–sort–undecorate) rather than inside the
794
+ // comparator.
795
+ return signatures
796
+ .map((signature) => ({
797
+ key: Hex.toBigInt(extractAddress({ payload: digest, signature })),
798
+ signature,
799
+ }))
800
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
801
+ .map((entry) => entry.signature);
802
+ }
650
803
  /**
651
804
  * Converts a signature envelope to RPC format.
652
805
  *
@@ -705,6 +858,17 @@ export function toRpc(envelope) {
705
858
  ...(keychain.version ? { version: keychain.version } : {}),
706
859
  };
707
860
  }
861
+ if (type === 'multisig') {
862
+ const multisig = envelope;
863
+ return {
864
+ type: 'multisig',
865
+ account: multisig.account,
866
+ configId: multisig.configId,
867
+ // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
868
+ signatures: multisig.signatures.map((signature) => serialize(signature)),
869
+ ...(multisig.init ? { init: multisig.init } : {}),
870
+ };
871
+ }
708
872
  throw new CoercionError({ envelope });
709
873
  }
710
874
  /**