@sovereign-sdk/multisig 0.3.1 → 0.4.0

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.
package/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@sovereign-sdk/multisig.svg)](https://www.npmjs.com/package/@sovereign-sdk/multisig)
4
4
  [![CI](https://github.com/Sovereign-Labs/sovereign-sdk/actions/workflows/typescript.ci.yml/badge.svg)](https://github.com/Sovereign-Labs/sovereign-sdk/actions/workflows/typescript.ci.yml)
5
5
 
6
- A TypeScript library for creating and managing multisig transactions on Sovereign SDK rollups.
6
+ Payload-agnostic multisig signer state for Sovereign SDK applications.
7
+
8
+ Use `@sovereign-sdk/multisig` when you want to collect signatures outside the rollup client and finalize them into a standard `TransactionV1` once the threshold is met.
7
9
 
8
10
  ## Installation
9
11
 
@@ -11,119 +13,46 @@ A TypeScript library for creating and managing multisig transactions on Sovereig
11
13
  npm install @sovereign-sdk/multisig
12
14
  ```
13
15
 
14
- ## Features
15
-
16
- - **Multisig Transaction Management**: Create and manage transactions requiring multiple signatures
17
- - **Threshold Signatures**: Configurable minimum signature requirements (M-of-N)
18
- - **Signature Collection**: Collect signatures from multiple parties before submission
19
- - **Transaction Conversion**: Convert between V0 (single-sig) and V1 (multisig) transaction formats
20
- - **Safety Checks**: Validates transaction consistency and prevents replay attacks
21
-
22
16
  ## Usage
23
17
 
24
- ### Basic Multisig Workflow
25
-
26
18
  ```typescript
27
- import { StandardRollup, createStandardRollup } from "@sovereign-sdk/web3";
28
- import { MultisigTransaction } from "@sovereign-sdk/multisig";
19
+ import { Multisig } from "@sovereign-sdk/multisig";
20
+ import { createStandardRollup } from "@sovereign-sdk/web3";
29
21
  import type { UnsignedTransaction } from "@sovereign-sdk/types";
22
+ import { bytesToHex } from "@sovereign-sdk/utils";
23
+
24
+ const rollup = await createStandardRollup<YourRuntimeCall>();
30
25
 
31
- // 1. Create an unsigned transaction
32
26
  const unsignedTx: UnsignedTransaction<YourRuntimeCall> = {
33
27
  runtime_call: {
34
28
  // Your rollup-specific call data
35
- value_setter: { set_value: { value: 100, gas: null } },
36
29
  },
37
- uniqueness: { nonce: 1 }, // Must be nonce-based for multisig
30
+ uniqueness: { nonce: 1 },
38
31
  details: {
39
- max_priority_fee_bips: 100,
32
+ max_priority_fee_bips: 0,
40
33
  max_fee: "1000000",
41
34
  gas_limit: null,
42
- chain_id: 4321,
35
+ chain_hash_fragment: "6654161651848106779",
43
36
  },
44
37
  };
45
38
 
46
- // 2. Initialize multisig with all participant public keys
47
- const allPubKeys = ["0xpubkey1", "0xpubkey2", "0xpubkey3"];
48
- const minSigners = 2; // 2-of-3 multisig
39
+ const multisig = Multisig.fromPubKeys(
40
+ ["pubkey1hex", "pubkey2hex", "pubkey3hex"],
41
+ 2,
42
+ );
49
43
 
50
- const multisig = MultisigTransaction.empty(unsignedTx, minSigners, allPubKeys);
44
+ const signingBytes = await rollup.multisigSigningBytes(unsignedTx, multisig);
45
+ multisig.addSignature(
46
+ bytesToHex(await signer1.sign(signingBytes)),
47
+ bytesToHex(await signer1.publicKey()),
48
+ );
51
49
 
52
- // 3. Add signatures as they're collected
53
- multisig.addSignature("0xsignature1", "0xpubkey1");
54
- multisig.addSignature("0xsignature2", "0xpubkey2");
50
+ multisig.addSignature(
51
+ bytesToHex(await signer2.sign(signingBytes)),
52
+ bytesToHex(await signer2.publicKey()),
53
+ );
55
54
 
56
- // 4. Check if ready for submission
57
55
  if (multisig.isComplete) {
58
- // Convert to rollup transaction for submission
59
- const finalTx = multisig.asTransaction();
60
- const rollup = await createStandardRollup();
61
- // Submit the transaction to the rollup
62
- const result = await rollup.submitTransaction(finalTx);
63
- console.log("Multisig transaction submitted", result);
56
+ await rollup.submitTransaction(multisig.toTransaction(unsignedTx));
64
57
  }
65
58
  ```
66
-
67
- ### Creating from Existing Signed Transactions
68
-
69
- ```typescript
70
- import { MultisigTransaction } from '@sovereign-sdk/multisig';
71
- import type { Transaction } from '@sovereign-sdk/types';
72
-
73
- // If you have individual V0 transactions from different signers
74
- const signedTxns: Transaction<YourRuntimeCall>[] = [
75
- {
76
- V0: {
77
- pub_key: "0xpubkey1",
78
- signature: "0xsignature1",
79
- runtime_call: /* same call data */,
80
- uniqueness: { nonce: 1 },
81
- details: /* same details */,
82
- }
83
- },
84
- {
85
- V0: {
86
- pub_key: "0xpubkey2",
87
- signature: "0xsignature2",
88
- runtime_call: /* same call data */,
89
- uniqueness: { nonce: 1 },
90
- details: /* same details */,
91
- }
92
- }
93
- ];
94
-
95
- // Create multisig from existing transactions
96
- const multisig = MultisigTransaction.fromTransactions({
97
- txns: signedTxns,
98
- minSigners: 2,
99
- allPubKeys: ["0xpubkey1", "0xpubkey2", "0xpubkey3"]
100
- });
101
- ```
102
-
103
- ### Adding Individual Signed Transactions
104
-
105
- ```typescript
106
- // Add a complete signed transaction to the multisig
107
- const signedTx: Transaction<YourRuntimeCall> = {
108
- V0: {
109
- pub_key: "0xpubkey3",
110
- signature: "0xsignature3",
111
- // ... same transaction data
112
- },
113
- };
114
-
115
- multisig.addSignedTransaction(signedTx);
116
-
117
- // Now check if complete and submit
118
- if (multisig.isComplete) {
119
- const finalTx = multisig.asTransaction();
120
- // Submit to rollup...
121
- }
122
- ```
123
-
124
- ## Important Notes
125
-
126
- - **Nonce-based transactions only**: Multisig currently only supports transactions with `{ nonce: number }` uniqueness
127
- - **Transaction consistency**: All signatures must be for the exact same unsigned transaction
128
- - **Public key validation**: Public keys must be known members of the multisig and can only sign once
129
-
package/dist/index.d.mts CHANGED
@@ -1,22 +1,12 @@
1
- import { UnsignedTransaction, SignatureAndPubKey, Transaction } from '@sovereign-sdk/types';
1
+ import { SignatureAndPubKey, UnsignedTransaction, TransactionV1 } from '@sovereign-sdk/types';
2
2
  import { HexString } from '@sovereign-sdk/utils';
3
3
 
4
+ declare const MAX_SIGNERS = 21;
4
5
  /**
5
6
  * Base error class for multisig-related errors.
6
7
  */
7
8
  declare class MultisigError extends Error {
8
9
  }
9
- /**
10
- * Error thrown when a transaction has an unsupported version for multisig operations.
11
- */
12
- declare class InvalidTransactionVersionError extends MultisigError {
13
- }
14
- /**
15
- * Error thrown when a provided transaction doesn't match the expected unsigned transaction.
16
- */
17
- declare class TransactionMismatchError extends Error {
18
- constructor();
19
- }
20
10
  /**
21
11
  * Error thrown when multisig is constructed or used with invalid parameters.
22
12
  */
@@ -24,113 +14,33 @@ declare class InvalidMultisigParameterError extends MultisigError {
24
14
  constructor(desc: string);
25
15
  }
26
16
  /**
27
- * Parameters for constructing a MultisigTransaction.
17
+ * Parameters for constructing a Multisig.
28
18
  */
29
19
  type MultisigParams = {
30
- /** The unsigned transaction to be signed by multiple parties */
31
- unsignedTx: UnsignedTransaction<unknown>;
32
20
  /** Array of signatures with pubkeys already collected */
33
- signatures: SignatureAndPubKey[];
21
+ signatures?: SignatureAndPubKey[];
34
22
  /** Array of public keys that haven't signed yet */
35
23
  unusedPubKeys: HexString[];
36
24
  /** Minimum number of signatures required */
37
25
  minSigners: number;
38
26
  };
39
27
  /**
40
- * Parameters for creating a MultisigTransaction from existing signed transactions.
28
+ * Represents multisig signer membership and collected signatures.
41
29
  */
42
- type FromTransactionsParams = {
43
- /** Array of signed transactions to extract signatures from */
44
- txns: Transaction<unknown>[];
45
- /** Minimum number of signatures required */
46
- minSigners: number;
47
- /** All public keys that are members of the multisig */
48
- allPubKeys: HexString[];
49
- };
50
- /**
51
- * Represents a multisig transaction that collects signatures from multiple parties.
52
- * Only supports nonce-based transactions and V0 transaction variants for signature collection.
53
- */
54
- declare class MultisigTransaction {
55
- private unsignedTx;
30
+ declare class Multisig {
56
31
  private signatures;
57
32
  private minSigners;
58
33
  private unusedPubKeys;
59
- /**
60
- * Creates a new MultisigTransaction instance.
61
- * @param params - The multisig parameters including unsigned transaction, signatures, and public keys
62
- */
63
- constructor({ unsignedTx, signatures, unusedPubKeys, minSigners, }: MultisigParams);
64
- /**
65
- * Creates an empty MultisigTransaction with no signatures collected yet.
66
- * @param unsignedTx - The unsigned transaction to be signed by multiple parties
67
- * @param minSigners - Minimum number of signatures required for completion
68
- * @param allPubKeys - All public keys that are members of the multisig
69
- * @returns A new empty MultisigTransaction instance
70
- */
71
- static empty(unsignedTx: UnsignedTransaction<unknown>, minSigners: number, allPubKeys: HexString[]): MultisigTransaction;
72
- /**
73
- * Creates a MultisigTransaction from an array of already-signed transactions.
74
- * Extracts signatures and validates that all transactions are identical except for signatures.
75
- * @param params - Parameters including the signed transactions, minimum signers, and all public keys
76
- * @returns A new MultisigTransaction instance
77
- * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant
78
- * @throws {TransactionMismatchError} If transactions don't match each other
79
- * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based
80
- */
81
- static fromTransactions({ txns, minSigners, allPubKeys, }: FromTransactionsParams): MultisigTransaction;
82
- /**
83
- * Adds a signature & public key from a signed transaction to the multisig transaction.
84
- * @param tx - The signed transaction to extract signature from
85
- * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant
86
- * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction
87
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
88
- */
89
- addSignedTransaction(tx: Transaction<unknown>): void;
90
- /**
91
- * Adds a signature to the multisig transaction.
92
- * @param signature - The signature to add
93
- * @param pubKey - The public key corresponding to the signature
94
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
95
- */
34
+ constructor({ signatures, unusedPubKeys, minSigners }: MultisigParams);
35
+ static fromPubKeys(allPubKeys: HexString[], minSigners: number): Multisig;
96
36
  addSignature(signature: HexString, pubKey: HexString): void;
97
- /**
98
- * Checks if the multisig has collected enough signatures to be complete.
99
- * @returns True if the number of signatures meets or exceeds the minimum required
100
- */
101
37
  get isComplete(): boolean;
102
- /**
103
- * Gets the unsigned transaction.
104
- * @returns A readonly view of the unsigned transaction
105
- */
106
- get unsignedTransaction(): Readonly<UnsignedTransaction<unknown>>;
107
- /**
108
- * Gets the signatures and public keys collected so far.
109
- * @returns A readonly view of the signatures and public keys
110
- */
38
+ get threshold(): number;
111
39
  get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]>;
112
- /**
113
- * Gets the remaining public keys that haven't signed yet.
114
- * @returns A readonly view of the remaining public keys
115
- */
116
40
  get remainingPubKeys(): Readonly<Set<HexString>>;
117
- /**
118
- * Gets the threshold number of signatures required for the multisig to be complete.
119
- * @returns The threshold number of signatures
120
- */
121
- get threshold(): number;
122
- /**
123
- * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.
124
- * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))
125
- * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)
126
- * @returns The 32-byte multisig address as a Uint8Array
127
- */
41
+ get allPubKeys(): Readonly<HexString[]>;
42
+ toTransaction<RuntimeCall>(unsignedTx: UnsignedTransaction<RuntimeCall>): TransactionV1<RuntimeCall>;
128
43
  getMultisigAddress(hasher?: "sha256"): Uint8Array;
129
- /**
130
- * Converts the multisig to a V1 transaction that can be submitted to the network.
131
- * @returns A V1 transaction containing all collected signatures and unused public keys
132
- */
133
- asTransaction(): Transaction<unknown>;
134
44
  }
135
45
 
136
- export { type FromTransactionsParams, InvalidMultisigParameterError, InvalidTransactionVersionError, MultisigError, type MultisigParams, MultisigTransaction, TransactionMismatchError };
46
+ export { InvalidMultisigParameterError, MAX_SIGNERS, Multisig, MultisigError, type MultisigParams };
package/dist/index.d.ts CHANGED
@@ -1,22 +1,12 @@
1
- import { UnsignedTransaction, SignatureAndPubKey, Transaction } from '@sovereign-sdk/types';
1
+ import { SignatureAndPubKey, UnsignedTransaction, TransactionV1 } from '@sovereign-sdk/types';
2
2
  import { HexString } from '@sovereign-sdk/utils';
3
3
 
4
+ declare const MAX_SIGNERS = 21;
4
5
  /**
5
6
  * Base error class for multisig-related errors.
6
7
  */
7
8
  declare class MultisigError extends Error {
8
9
  }
9
- /**
10
- * Error thrown when a transaction has an unsupported version for multisig operations.
11
- */
12
- declare class InvalidTransactionVersionError extends MultisigError {
13
- }
14
- /**
15
- * Error thrown when a provided transaction doesn't match the expected unsigned transaction.
16
- */
17
- declare class TransactionMismatchError extends Error {
18
- constructor();
19
- }
20
10
  /**
21
11
  * Error thrown when multisig is constructed or used with invalid parameters.
22
12
  */
@@ -24,113 +14,33 @@ declare class InvalidMultisigParameterError extends MultisigError {
24
14
  constructor(desc: string);
25
15
  }
26
16
  /**
27
- * Parameters for constructing a MultisigTransaction.
17
+ * Parameters for constructing a Multisig.
28
18
  */
29
19
  type MultisigParams = {
30
- /** The unsigned transaction to be signed by multiple parties */
31
- unsignedTx: UnsignedTransaction<unknown>;
32
20
  /** Array of signatures with pubkeys already collected */
33
- signatures: SignatureAndPubKey[];
21
+ signatures?: SignatureAndPubKey[];
34
22
  /** Array of public keys that haven't signed yet */
35
23
  unusedPubKeys: HexString[];
36
24
  /** Minimum number of signatures required */
37
25
  minSigners: number;
38
26
  };
39
27
  /**
40
- * Parameters for creating a MultisigTransaction from existing signed transactions.
28
+ * Represents multisig signer membership and collected signatures.
41
29
  */
42
- type FromTransactionsParams = {
43
- /** Array of signed transactions to extract signatures from */
44
- txns: Transaction<unknown>[];
45
- /** Minimum number of signatures required */
46
- minSigners: number;
47
- /** All public keys that are members of the multisig */
48
- allPubKeys: HexString[];
49
- };
50
- /**
51
- * Represents a multisig transaction that collects signatures from multiple parties.
52
- * Only supports nonce-based transactions and V0 transaction variants for signature collection.
53
- */
54
- declare class MultisigTransaction {
55
- private unsignedTx;
30
+ declare class Multisig {
56
31
  private signatures;
57
32
  private minSigners;
58
33
  private unusedPubKeys;
59
- /**
60
- * Creates a new MultisigTransaction instance.
61
- * @param params - The multisig parameters including unsigned transaction, signatures, and public keys
62
- */
63
- constructor({ unsignedTx, signatures, unusedPubKeys, minSigners, }: MultisigParams);
64
- /**
65
- * Creates an empty MultisigTransaction with no signatures collected yet.
66
- * @param unsignedTx - The unsigned transaction to be signed by multiple parties
67
- * @param minSigners - Minimum number of signatures required for completion
68
- * @param allPubKeys - All public keys that are members of the multisig
69
- * @returns A new empty MultisigTransaction instance
70
- */
71
- static empty(unsignedTx: UnsignedTransaction<unknown>, minSigners: number, allPubKeys: HexString[]): MultisigTransaction;
72
- /**
73
- * Creates a MultisigTransaction from an array of already-signed transactions.
74
- * Extracts signatures and validates that all transactions are identical except for signatures.
75
- * @param params - Parameters including the signed transactions, minimum signers, and all public keys
76
- * @returns A new MultisigTransaction instance
77
- * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant
78
- * @throws {TransactionMismatchError} If transactions don't match each other
79
- * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based
80
- */
81
- static fromTransactions({ txns, minSigners, allPubKeys, }: FromTransactionsParams): MultisigTransaction;
82
- /**
83
- * Adds a signature & public key from a signed transaction to the multisig transaction.
84
- * @param tx - The signed transaction to extract signature from
85
- * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant
86
- * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction
87
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
88
- */
89
- addSignedTransaction(tx: Transaction<unknown>): void;
90
- /**
91
- * Adds a signature to the multisig transaction.
92
- * @param signature - The signature to add
93
- * @param pubKey - The public key corresponding to the signature
94
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
95
- */
34
+ constructor({ signatures, unusedPubKeys, minSigners }: MultisigParams);
35
+ static fromPubKeys(allPubKeys: HexString[], minSigners: number): Multisig;
96
36
  addSignature(signature: HexString, pubKey: HexString): void;
97
- /**
98
- * Checks if the multisig has collected enough signatures to be complete.
99
- * @returns True if the number of signatures meets or exceeds the minimum required
100
- */
101
37
  get isComplete(): boolean;
102
- /**
103
- * Gets the unsigned transaction.
104
- * @returns A readonly view of the unsigned transaction
105
- */
106
- get unsignedTransaction(): Readonly<UnsignedTransaction<unknown>>;
107
- /**
108
- * Gets the signatures and public keys collected so far.
109
- * @returns A readonly view of the signatures and public keys
110
- */
38
+ get threshold(): number;
111
39
  get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]>;
112
- /**
113
- * Gets the remaining public keys that haven't signed yet.
114
- * @returns A readonly view of the remaining public keys
115
- */
116
40
  get remainingPubKeys(): Readonly<Set<HexString>>;
117
- /**
118
- * Gets the threshold number of signatures required for the multisig to be complete.
119
- * @returns The threshold number of signatures
120
- */
121
- get threshold(): number;
122
- /**
123
- * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.
124
- * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))
125
- * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)
126
- * @returns The 32-byte multisig address as a Uint8Array
127
- */
41
+ get allPubKeys(): Readonly<HexString[]>;
42
+ toTransaction<RuntimeCall>(unsignedTx: UnsignedTransaction<RuntimeCall>): TransactionV1<RuntimeCall>;
128
43
  getMultisigAddress(hasher?: "sha256"): Uint8Array;
129
- /**
130
- * Converts the multisig to a V1 transaction that can be submitted to the network.
131
- * @returns A V1 transaction containing all collected signatures and unused public keys
132
- */
133
- asTransaction(): Transaction<unknown>;
134
44
  }
135
45
 
136
- export { type FromTransactionsParams, InvalidMultisigParameterError, InvalidTransactionVersionError, MultisigError, type MultisigParams, MultisigTransaction, TransactionMismatchError };
46
+ export { InvalidMultisigParameterError, MAX_SIGNERS, Multisig, MultisigError, type MultisigParams };
package/dist/index.js CHANGED
@@ -31,178 +31,92 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  InvalidMultisigParameterError: () => InvalidMultisigParameterError,
34
- InvalidTransactionVersionError: () => InvalidTransactionVersionError,
35
- MultisigError: () => MultisigError,
36
- MultisigTransaction: () => MultisigTransaction,
37
- TransactionMismatchError: () => TransactionMismatchError
34
+ MAX_SIGNERS: () => MAX_SIGNERS,
35
+ Multisig: () => Multisig,
36
+ MultisigError: () => MultisigError
38
37
  });
39
38
  module.exports = __toCommonJS(src_exports);
40
39
  var import_sha2 = require("@noble/hashes/sha2");
41
40
  var import_utils = require("@sovereign-sdk/utils");
42
41
  var borsh = __toESM(require("borsh"));
42
+ var MAX_SIGNERS = 21;
43
43
  var MultisigError = class extends Error {
44
44
  };
45
- var InvalidTransactionVersionError = class extends MultisigError {
46
- };
47
- var TransactionMismatchError = class extends Error {
48
- constructor() {
49
- super(
50
- "Provided transaction does not match the expected unsigned transaction"
51
- );
52
- }
53
- };
54
45
  var InvalidMultisigParameterError = class extends MultisigError {
55
46
  constructor(desc) {
56
47
  super(`Multisig was constructed with invalid parameter: ${desc}`);
57
48
  }
58
49
  };
59
- var MultisigTransaction = class _MultisigTransaction {
60
- /**
61
- * Creates a new MultisigTransaction instance.
62
- * @param params - The multisig parameters including unsigned transaction, signatures, and public keys
63
- */
64
- constructor({
65
- unsignedTx,
66
- signatures,
67
- unusedPubKeys,
68
- minSigners
69
- }) {
70
- this.signatures = [];
71
- this.unusedPubKeys = /* @__PURE__ */ new Set();
72
- this.unsignedTx = unsignedTx;
73
- this.signatures = signatures;
74
- this.unusedPubKeys = new Set(unusedPubKeys);
50
+ var Multisig = class _Multisig {
51
+ constructor({ signatures = [], unusedPubKeys, minSigners }) {
52
+ const normalizedSignatures = signatures.map(normalizeSignatureAndPubKey);
53
+ const normalizedUnusedPubKeys = unusedPubKeys.map(
54
+ (pubKey) => normalizeMultisigHex("public key", pubKey)
55
+ );
56
+ assertValidSignerSet(
57
+ normalizedSignatures,
58
+ normalizedUnusedPubKeys,
59
+ minSigners
60
+ );
61
+ this.signatures = normalizedSignatures;
62
+ this.unusedPubKeys = new Set(normalizedUnusedPubKeys);
75
63
  this.minSigners = minSigners;
76
64
  }
77
- /**
78
- * Creates an empty MultisigTransaction with no signatures collected yet.
79
- * @param unsignedTx - The unsigned transaction to be signed by multiple parties
80
- * @param minSigners - Minimum number of signatures required for completion
81
- * @param allPubKeys - All public keys that are members of the multisig
82
- * @returns A new empty MultisigTransaction instance
83
- */
84
- static empty(unsignedTx, minSigners, allPubKeys) {
85
- return new _MultisigTransaction({
86
- unsignedTx,
65
+ static fromPubKeys(allPubKeys, minSigners) {
66
+ return new _Multisig({
87
67
  signatures: [],
88
68
  unusedPubKeys: allPubKeys,
89
69
  minSigners
90
70
  });
91
71
  }
92
- /**
93
- * Creates a MultisigTransaction from an array of already-signed transactions.
94
- * Extracts signatures and validates that all transactions are identical except for signatures.
95
- * @param params - Parameters including the signed transactions, minimum signers, and all public keys
96
- * @returns A new MultisigTransaction instance
97
- * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant
98
- * @throws {TransactionMismatchError} If transactions don't match each other
99
- * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based
100
- */
101
- static fromTransactions({
102
- txns,
103
- minSigners,
104
- allPubKeys
105
- }) {
106
- const signatures = [];
107
- const unsignedTx = asUnsignedTransaction(txns[0]);
108
- const unusedPubKeys = new Set(allPubKeys);
109
- assertIsNonceBasedTx(unsignedTx);
110
- for (const tx of txns) {
111
- assertTxVariant(tx);
112
- assertTxMatchesUnsignedTx(tx, unsignedTx);
113
- const { pub_key, signature } = tx.V0;
114
- if (!unusedPubKeys.delete(pub_key)) {
115
- throw new InvalidMultisigParameterError(
116
- `Public key is not a member of the multisig or has already signed: ${pub_key}`
117
- );
118
- }
119
- signatures.push({ pub_key, signature });
120
- }
121
- return new _MultisigTransaction({
122
- unsignedTx,
123
- signatures,
124
- unusedPubKeys: Array.from(unusedPubKeys),
125
- minSigners
126
- });
127
- }
128
- /**
129
- * Adds a signature & public key from a signed transaction to the multisig transaction.
130
- * @param tx - The signed transaction to extract signature from
131
- * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant
132
- * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction
133
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
134
- */
135
- addSignedTransaction(tx) {
136
- assertTxVariant(tx);
137
- assertTxMatchesUnsignedTx(tx, this.unsignedTx);
138
- const { pub_key, signature } = tx.V0;
139
- this.addSignature(signature, pub_key);
140
- }
141
- /**
142
- * Adds a signature to the multisig transaction.
143
- * @param signature - The signature to add
144
- * @param pubKey - The public key corresponding to the signature
145
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
146
- */
147
72
  addSignature(signature, pubKey) {
148
- if (!this.unusedPubKeys.delete(pubKey)) {
73
+ const pair = normalizeSignatureAndPubKey({ signature, pub_key: pubKey });
74
+ if (!this.unusedPubKeys.delete(pair.pub_key)) {
149
75
  throw new InvalidMultisigParameterError(
150
- `Public key is not a member of the multisig or has already signed: ${pubKey}`
76
+ `Public key is not a member of the multisig or has already signed: ${pair.pub_key}`
151
77
  );
152
78
  }
153
- this.signatures.push({ pub_key: pubKey, signature });
79
+ this.signatures.push(pair);
154
80
  }
155
- /**
156
- * Checks if the multisig has collected enough signatures to be complete.
157
- * @returns True if the number of signatures meets or exceeds the minimum required
158
- */
159
81
  get isComplete() {
160
82
  return this.signatures.length >= this.minSigners;
161
83
  }
162
- /**
163
- * Gets the unsigned transaction.
164
- * @returns A readonly view of the unsigned transaction
165
- */
166
- get unsignedTransaction() {
167
- return this.unsignedTx;
84
+ get threshold() {
85
+ return this.minSigners;
168
86
  }
169
- /**
170
- * Gets the signatures and public keys collected so far.
171
- * @returns A readonly view of the signatures and public keys
172
- */
173
87
  get signaturesAndPubKeys() {
174
- return this.signatures;
88
+ return this.signatures.map((pair) => ({ ...pair }));
175
89
  }
176
- /**
177
- * Gets the remaining public keys that haven't signed yet.
178
- * @returns A readonly view of the remaining public keys
179
- */
180
90
  get remainingPubKeys() {
181
- return this.unusedPubKeys;
91
+ return new Set(this.unusedPubKeys);
182
92
  }
183
- /**
184
- * Gets the threshold number of signatures required for the multisig to be complete.
185
- * @returns The threshold number of signatures
186
- */
187
- get threshold() {
188
- return this.minSigners;
93
+ get allPubKeys() {
94
+ return [
95
+ ...this.signatures.map((signature) => signature.pub_key),
96
+ ...this.unusedPubKeys
97
+ ];
98
+ }
99
+ toTransaction(unsignedTx) {
100
+ if (!this.isComplete) {
101
+ throw new MultisigError("Multisig transaction is incomplete");
102
+ }
103
+ return {
104
+ V1: {
105
+ ...unsignedTx,
106
+ signatures: [...this.signaturesAndPubKeys],
107
+ unused_pub_keys: [...this.remainingPubKeys],
108
+ min_signers: this.threshold
109
+ }
110
+ };
189
111
  }
190
- /**
191
- * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.
192
- * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))
193
- * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)
194
- * @returns The 32-byte multisig address as a Uint8Array
195
- */
196
112
  getMultisigAddress(hasher = "sha256") {
197
113
  if (hasher !== "sha256") {
198
114
  throw new Error(`Unsupported hasher: ${hasher}`);
199
115
  }
200
- const allPubKeys = [
201
- ...this.signatures.map((s) => s.pub_key),
202
- ...Array.from(this.unusedPubKeys)
203
- ];
204
- const sortedPubKeys = allPubKeys.sort();
205
- const pubKeyBytes = sortedPubKeys.map((pk) => Array.from((0, import_utils.hexToBytes)(pk)));
116
+ const sortedPubKeys = [...this.allPubKeys].sort();
117
+ const pubKeyBytes = sortedPubKeys.map(
118
+ (pubKey) => Array.from((0, import_utils.hexToBytes)(pubKey))
119
+ );
206
120
  const buffer = new borsh.BinaryWriter();
207
121
  buffer.writeU8(this.minSigners);
208
122
  buffer.writeArray(pubKeyBytes, (pkBytes) => {
@@ -210,68 +124,58 @@ var MultisigTransaction = class _MultisigTransaction {
210
124
  });
211
125
  return (0, import_sha2.sha256)(buffer.toArray());
212
126
  }
213
- /**
214
- * Converts the multisig to a V1 transaction that can be submitted to the network.
215
- * @returns A V1 transaction containing all collected signatures and unused public keys
216
- */
217
- asTransaction() {
218
- return {
219
- V1: {
220
- runtime_call: this.unsignedTx.runtime_call,
221
- uniqueness: this.unsignedTx.uniqueness,
222
- details: this.unsignedTx.details,
223
- unused_pub_keys: Array.from(this.unusedPubKeys),
224
- signatures: this.signatures,
225
- min_signers: this.minSigners
226
- }
227
- };
228
- }
229
127
  };
230
- function assertTxVariant(tx) {
231
- if ("V1" in tx) {
232
- throw new InvalidTransactionVersionError(
233
- "Input transaction was an unsupported variant (V1)"
128
+ function assertValidSignerSet(signatures, unusedPubKeys, minSigners) {
129
+ const seenPubKeys = /* @__PURE__ */ new Set();
130
+ const allPubKeys = [
131
+ ...signatures.map((signature) => signature.pub_key),
132
+ ...unusedPubKeys
133
+ ];
134
+ if (!Number.isInteger(minSigners) || minSigners < 1) {
135
+ throw new InvalidMultisigParameterError(
136
+ `minSigners must be a positive integer, got ${minSigners}`
234
137
  );
235
138
  }
236
- }
237
- function asUnsignedTransaction(tx) {
238
- if ("V0" in tx) {
239
- return {
240
- runtime_call: tx.V0.runtime_call,
241
- uniqueness: tx.V0.uniqueness,
242
- details: tx.V0.details
243
- };
139
+ if (allPubKeys.length < 2 || allPubKeys.length > MAX_SIGNERS) {
140
+ throw new InvalidMultisigParameterError(
141
+ `expected 2-${MAX_SIGNERS} total signers, got ${allPubKeys.length}`
142
+ );
244
143
  }
245
- if ("V1" in tx) {
246
- return {
247
- runtime_call: tx.V1.runtime_call,
248
- uniqueness: tx.V1.uniqueness,
249
- details: tx.V1.details
250
- };
144
+ if (minSigners > allPubKeys.length) {
145
+ throw new InvalidMultisigParameterError(
146
+ `minSigners cannot exceed total signer count: ${minSigners} > ${allPubKeys.length}`
147
+ );
251
148
  }
252
- throw new InvalidTransactionVersionError(
253
- "Transaction variant is neither V0 nor V1"
254
- );
255
- }
256
- function assertTxMatchesUnsignedTx(tx, unsignedTx) {
257
- const txAsUnsigned = asUnsignedTransaction(tx);
258
- if (JSON.stringify(txAsUnsigned) !== JSON.stringify(unsignedTx)) {
259
- throw new TransactionMismatchError();
149
+ for (const pubKey of allPubKeys) {
150
+ if (seenPubKeys.has(pubKey)) {
151
+ throw new InvalidMultisigParameterError(
152
+ `Duplicate multisig public key: ${pubKey}`
153
+ );
154
+ }
155
+ seenPubKeys.add(pubKey);
260
156
  }
261
157
  }
262
- function assertIsNonceBasedTx(unsignedTx) {
263
- if (!("nonce" in unsignedTx.uniqueness)) {
158
+ function normalizeSignatureAndPubKey(pair) {
159
+ return {
160
+ signature: normalizeMultisigHex("signature", pair.signature),
161
+ pub_key: normalizeMultisigHex("public key", pair.pub_key)
162
+ };
163
+ }
164
+ function normalizeMultisigHex(kind, value) {
165
+ try {
166
+ return (0, import_utils.normalizeHexString)(value);
167
+ } catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
264
169
  throw new InvalidMultisigParameterError(
265
- "Only nonce-based transactions are supported for multisig"
170
+ `Invalid multisig ${kind} ${value}: ${message}`
266
171
  );
267
172
  }
268
173
  }
269
174
  // Annotate the CommonJS export names for ESM import in node:
270
175
  0 && (module.exports = {
271
176
  InvalidMultisigParameterError,
272
- InvalidTransactionVersionError,
273
- MultisigError,
274
- MultisigTransaction,
275
- TransactionMismatchError
177
+ MAX_SIGNERS,
178
+ Multisig,
179
+ MultisigError
276
180
  });
277
181
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2\";\nimport type {\n SignatureAndPubKey,\n Transaction,\n TransactionV0,\n UnsignedTransaction,\n} from \"@sovereign-sdk/types\";\nimport type { HexString } from \"@sovereign-sdk/utils\";\nimport { bytesToHex, hexToBytes } from \"@sovereign-sdk/utils\";\nimport * as borsh from \"borsh\";\n\n/**\n * Base error class for multisig-related errors.\n */\nexport class MultisigError extends Error {}\n\n/**\n * Error thrown when a transaction has an unsupported version for multisig operations.\n */\nexport class InvalidTransactionVersionError extends MultisigError {}\n\n/**\n * Error thrown when a provided transaction doesn't match the expected unsigned transaction.\n */\nexport class TransactionMismatchError extends Error {\n constructor() {\n super(\n \"Provided transaction does not match the expected unsigned transaction\",\n );\n }\n}\n\n/**\n * Error thrown when multisig is constructed or used with invalid parameters.\n */\nexport class InvalidMultisigParameterError extends MultisigError {\n constructor(desc: string) {\n super(`Multisig was constructed with invalid parameter: ${desc}`);\n }\n}\n\n/**\n * Parameters for constructing a MultisigTransaction.\n */\nexport type MultisigParams = {\n /** The unsigned transaction to be signed by multiple parties */\n unsignedTx: UnsignedTransaction<unknown>;\n /** Array of signatures with pubkeys already collected */\n signatures: SignatureAndPubKey[];\n /** Array of public keys that haven't signed yet */\n unusedPubKeys: HexString[];\n /** Minimum number of signatures required */\n minSigners: number;\n};\n\n/**\n * Parameters for creating a MultisigTransaction from existing signed transactions.\n */\nexport type FromTransactionsParams = {\n /** Array of signed transactions to extract signatures from */\n txns: Transaction<unknown>[];\n /** Minimum number of signatures required */\n minSigners: number;\n /** All public keys that are members of the multisig */\n allPubKeys: HexString[];\n};\n\n/**\n * Represents a multisig transaction that collects signatures from multiple parties.\n * Only supports nonce-based transactions and V0 transaction variants for signature collection.\n */\nexport class MultisigTransaction {\n private unsignedTx: UnsignedTransaction<unknown>;\n private signatures: SignatureAndPubKey[] = [];\n private minSigners: number;\n private unusedPubKeys = new Set<HexString>();\n\n /**\n * Creates a new MultisigTransaction instance.\n * @param params - The multisig parameters including unsigned transaction, signatures, and public keys\n */\n constructor({\n unsignedTx,\n signatures,\n unusedPubKeys,\n minSigners,\n }: MultisigParams) {\n this.unsignedTx = unsignedTx;\n this.signatures = signatures;\n this.unusedPubKeys = new Set(unusedPubKeys);\n this.minSigners = minSigners;\n }\n\n /**\n * Creates an empty MultisigTransaction with no signatures collected yet.\n * @param unsignedTx - The unsigned transaction to be signed by multiple parties\n * @param minSigners - Minimum number of signatures required for completion\n * @param allPubKeys - All public keys that are members of the multisig\n * @returns A new empty MultisigTransaction instance\n */\n static empty(\n unsignedTx: UnsignedTransaction<unknown>,\n minSigners: number,\n allPubKeys: HexString[],\n ): MultisigTransaction {\n return new MultisigTransaction({\n unsignedTx,\n signatures: [],\n unusedPubKeys: allPubKeys,\n minSigners,\n });\n }\n\n /**\n * Creates a MultisigTransaction from an array of already-signed transactions.\n * Extracts signatures and validates that all transactions are identical except for signatures.\n * @param params - Parameters including the signed transactions, minimum signers, and all public keys\n * @returns A new MultisigTransaction instance\n * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant\n * @throws {TransactionMismatchError} If transactions don't match each other\n * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based\n */\n static fromTransactions({\n txns,\n minSigners,\n allPubKeys,\n }: FromTransactionsParams): MultisigTransaction {\n const signatures: SignatureAndPubKey[] = [];\n const unsignedTx = asUnsignedTransaction(txns[0]);\n const unusedPubKeys = new Set<HexString>(allPubKeys);\n\n assertIsNonceBasedTx(unsignedTx);\n\n for (const tx of txns) {\n assertTxVariant(tx);\n assertTxMatchesUnsignedTx(tx, unsignedTx);\n\n const { pub_key, signature } = tx.V0;\n\n if (!unusedPubKeys.delete(pub_key)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pub_key}`,\n );\n }\n\n signatures.push({ pub_key, signature });\n }\n\n return new MultisigTransaction({\n unsignedTx,\n signatures,\n unusedPubKeys: Array.from(unusedPubKeys),\n minSigners,\n });\n }\n\n /**\n * Adds a signature & public key from a signed transaction to the multisig transaction.\n * @param tx - The signed transaction to extract signature from\n * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant\n * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction\n * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed\n */\n addSignedTransaction(tx: Transaction<unknown>): void {\n assertTxVariant(tx);\n assertTxMatchesUnsignedTx(tx, this.unsignedTx);\n\n const { pub_key, signature } = tx.V0;\n\n this.addSignature(signature, pub_key);\n }\n\n /**\n * Adds a signature to the multisig transaction.\n * @param signature - The signature to add\n * @param pubKey - The public key corresponding to the signature\n * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed\n */\n addSignature(signature: HexString, pubKey: HexString): void {\n if (!this.unusedPubKeys.delete(pubKey)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pubKey}`,\n );\n }\n\n this.signatures.push({ pub_key: pubKey, signature });\n }\n\n /**\n * Checks if the multisig has collected enough signatures to be complete.\n * @returns True if the number of signatures meets or exceeds the minimum required\n */\n get isComplete(): boolean {\n return this.signatures.length >= this.minSigners;\n }\n\n /**\n * Gets the unsigned transaction.\n * @returns A readonly view of the unsigned transaction\n */\n get unsignedTransaction(): Readonly<UnsignedTransaction<unknown>> {\n return this.unsignedTx;\n }\n\n /**\n * Gets the signatures and public keys collected so far.\n * @returns A readonly view of the signatures and public keys\n */\n get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]> {\n return this.signatures;\n }\n\n /**\n * Gets the remaining public keys that haven't signed yet.\n * @returns A readonly view of the remaining public keys\n */\n get remainingPubKeys(): Readonly<Set<HexString>> {\n return this.unusedPubKeys;\n }\n\n /**\n * Gets the threshold number of signatures required for the multisig to be complete.\n * @returns The threshold number of signatures\n */\n get threshold(): number {\n return this.minSigners;\n }\n\n /**\n * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.\n * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))\n * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)\n * @returns The 32-byte multisig address as a Uint8Array\n */\n getMultisigAddress(hasher: \"sha256\" = \"sha256\"): Uint8Array {\n if (hasher !== \"sha256\") {\n throw new Error(`Unsupported hasher: ${hasher}`);\n }\n\n // Collect all public keys (signatures + unused)\n const allPubKeys = [\n ...this.signatures.map((s) => s.pub_key),\n ...Array.from(this.unusedPubKeys),\n ];\n const sortedPubKeys = allPubKeys.sort();\n const pubKeyBytes = sortedPubKeys.map((pk) => Array.from(hexToBytes(pk)));\n const buffer = new borsh.BinaryWriter();\n\n buffer.writeU8(this.minSigners);\n buffer.writeArray(pubKeyBytes, (pkBytes: number[]) => {\n buffer.writeFixedArray(new Uint8Array(pkBytes));\n });\n\n return sha256(buffer.toArray());\n }\n\n /**\n * Converts the multisig to a V1 transaction that can be submitted to the network.\n * @returns A V1 transaction containing all collected signatures and unused public keys\n */\n asTransaction(): Transaction<unknown> {\n return {\n V1: {\n runtime_call: this.unsignedTx.runtime_call,\n uniqueness: this.unsignedTx.uniqueness,\n details: this.unsignedTx.details,\n unused_pub_keys: Array.from(this.unusedPubKeys),\n signatures: this.signatures,\n min_signers: this.minSigners,\n },\n };\n }\n}\n\nfunction assertTxVariant(\n tx: Transaction<unknown>,\n): asserts tx is TransactionV0<unknown> {\n if (\"V1\" in tx) {\n throw new InvalidTransactionVersionError(\n \"Input transaction was an unsupported variant (V1)\",\n );\n }\n}\n\nfunction asUnsignedTransaction(\n tx: Transaction<unknown>,\n): UnsignedTransaction<unknown> {\n if (\"V0\" in tx) {\n return {\n runtime_call: tx.V0.runtime_call,\n uniqueness: tx.V0.uniqueness,\n details: tx.V0.details,\n };\n }\n\n if (\"V1\" in tx) {\n return {\n runtime_call: tx.V1.runtime_call,\n uniqueness: tx.V1.uniqueness,\n details: tx.V1.details,\n };\n }\n\n throw new InvalidTransactionVersionError(\n \"Transaction variant is neither V0 nor V1\",\n );\n}\n\nfunction assertTxMatchesUnsignedTx(\n tx: Transaction<unknown>,\n unsignedTx: UnsignedTransaction<unknown>,\n): void {\n const txAsUnsigned = asUnsignedTransaction(tx);\n\n if (JSON.stringify(txAsUnsigned) !== JSON.stringify(unsignedTx)) {\n throw new TransactionMismatchError();\n }\n}\n\nfunction assertIsNonceBasedTx(unsignedTx: UnsignedTransaction<unknown>): void {\n if (!(\"nonce\" in unsignedTx.uniqueness)) {\n throw new InvalidMultisigParameterError(\n \"Only nonce-based transactions are supported for multisig\",\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAuB;AAQvB,mBAAuC;AACvC,YAAuB;AAKhB,IAAM,gBAAN,cAA4B,MAAM;AAAC;AAKnC,IAAM,iCAAN,cAA6C,cAAc;AAAC;AAK5D,IAAM,2BAAN,cAAuC,MAAM;AAAA,EAClD,cAAc;AACZ;AAAA,MACE;AAAA,IACF;AAAA,EACF;AACF;AAKO,IAAM,gCAAN,cAA4C,cAAc;AAAA,EAC/D,YAAY,MAAc;AACxB,UAAM,oDAAoD,IAAI,EAAE;AAAA,EAClE;AACF;AAgCO,IAAM,sBAAN,MAAM,qBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA,EAU/B,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAmB;AAbnB,SAAQ,aAAmC,CAAC;AAE5C,SAAQ,gBAAgB,oBAAI,IAAe;AAYzC,SAAK,aAAa;AAClB,SAAK,aAAa;AAClB,SAAK,gBAAgB,IAAI,IAAI,aAAa;AAC1C,SAAK,aAAa;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAO,MACL,YACA,YACA,YACqB;AACrB,WAAO,IAAI,qBAAoB;AAAA,MAC7B;AAAA,MACA,YAAY,CAAC;AAAA,MACb,eAAe;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,OAAO,iBAAiB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAgD;AAC9C,UAAM,aAAmC,CAAC;AAC1C,UAAM,aAAa,sBAAsB,KAAK,CAAC,CAAC;AAChD,UAAM,gBAAgB,IAAI,IAAe,UAAU;AAEnD,yBAAqB,UAAU;AAE/B,eAAW,MAAM,MAAM;AACrB,sBAAgB,EAAE;AAClB,gCAA0B,IAAI,UAAU;AAExC,YAAM,EAAE,SAAS,UAAU,IAAI,GAAG;AAElC,UAAI,CAAC,cAAc,OAAO,OAAO,GAAG;AAClC,cAAM,IAAI;AAAA,UACR,qEAAqE,OAAO;AAAA,QAC9E;AAAA,MACF;AAEA,iBAAW,KAAK,EAAE,SAAS,UAAU,CAAC;AAAA,IACxC;AAEA,WAAO,IAAI,qBAAoB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,eAAe,MAAM,KAAK,aAAa;AAAA,MACvC;AAAA,IACF,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,qBAAqB,IAAgC;AACnD,oBAAgB,EAAE;AAClB,8BAA0B,IAAI,KAAK,UAAU;AAE7C,UAAM,EAAE,SAAS,UAAU,IAAI,GAAG;AAElC,SAAK,aAAa,WAAW,OAAO;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,aAAa,WAAsB,QAAyB;AAC1D,QAAI,CAAC,KAAK,cAAc,OAAO,MAAM,GAAG;AACtC,YAAM,IAAI;AAAA,QACR,qEAAqE,MAAM;AAAA,MAC7E;AAAA,IACF;AAEA,SAAK,WAAW,KAAK,EAAE,SAAS,QAAQ,UAAU,CAAC;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,aAAsB;AACxB,WAAO,KAAK,WAAW,UAAU,KAAK;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,sBAA8D;AAChE,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,uBAAuD;AACzD,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,mBAA6C;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAI,YAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,mBAAmB,SAAmB,UAAsB;AAC1D,QAAI,WAAW,UAAU;AACvB,YAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;AAAA,IACjD;AAGA,UAAM,aAAa;AAAA,MACjB,GAAG,KAAK,WAAW,IAAI,CAAC,MAAM,EAAE,OAAO;AAAA,MACvC,GAAG,MAAM,KAAK,KAAK,aAAa;AAAA,IAClC;AACA,UAAM,gBAAgB,WAAW,KAAK;AACtC,UAAM,cAAc,cAAc,IAAI,CAAC,OAAO,MAAM,SAAK,yBAAW,EAAE,CAAC,CAAC;AACxE,UAAM,SAAS,IAAU,mBAAa;AAEtC,WAAO,QAAQ,KAAK,UAAU;AAC9B,WAAO,WAAW,aAAa,CAAC,YAAsB;AACpD,aAAO,gBAAgB,IAAI,WAAW,OAAO,CAAC;AAAA,IAChD,CAAC;AAED,eAAO,oBAAO,OAAO,QAAQ,CAAC;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAsC;AACpC,WAAO;AAAA,MACL,IAAI;AAAA,QACF,cAAc,KAAK,WAAW;AAAA,QAC9B,YAAY,KAAK,WAAW;AAAA,QAC5B,SAAS,KAAK,WAAW;AAAA,QACzB,iBAAiB,MAAM,KAAK,KAAK,aAAa;AAAA,QAC9C,YAAY,KAAK;AAAA,QACjB,aAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,gBACP,IACsC;AACtC,MAAI,QAAQ,IAAI;AACd,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,sBACP,IAC8B;AAC9B,MAAI,QAAQ,IAAI;AACd,WAAO;AAAA,MACL,cAAc,GAAG,GAAG;AAAA,MACpB,YAAY,GAAG,GAAG;AAAA,MAClB,SAAS,GAAG,GAAG;AAAA,IACjB;AAAA,EACF;AAEA,MAAI,QAAQ,IAAI;AACd,WAAO;AAAA,MACL,cAAc,GAAG,GAAG;AAAA,MACpB,YAAY,GAAG,GAAG;AAAA,MAClB,SAAS,GAAG,GAAG;AAAA,IACjB;AAAA,EACF;AAEA,QAAM,IAAI;AAAA,IACR;AAAA,EACF;AACF;AAEA,SAAS,0BACP,IACA,YACM;AACN,QAAM,eAAe,sBAAsB,EAAE;AAE7C,MAAI,KAAK,UAAU,YAAY,MAAM,KAAK,UAAU,UAAU,GAAG;AAC/D,UAAM,IAAI,yBAAyB;AAAA,EACrC;AACF;AAEA,SAAS,qBAAqB,YAAgD;AAC5E,MAAI,EAAE,WAAW,WAAW,aAAa;AACvC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2\";\nimport type {\n SignatureAndPubKey,\n TransactionV1,\n UnsignedTransaction,\n} from \"@sovereign-sdk/types\";\nimport type { HexString } from \"@sovereign-sdk/utils\";\nimport { hexToBytes, normalizeHexString } from \"@sovereign-sdk/utils\";\nimport * as borsh from \"borsh\";\n\nexport const MAX_SIGNERS = 21;\n\n/**\n * Base error class for multisig-related errors.\n */\nexport class MultisigError extends Error {}\n\n/**\n * Error thrown when multisig is constructed or used with invalid parameters.\n */\nexport class InvalidMultisigParameterError extends MultisigError {\n constructor(desc: string) {\n super(`Multisig was constructed with invalid parameter: ${desc}`);\n }\n}\n\n/**\n * Parameters for constructing a Multisig.\n */\nexport type MultisigParams = {\n /** Array of signatures with pubkeys already collected */\n signatures?: SignatureAndPubKey[];\n /** Array of public keys that haven't signed yet */\n unusedPubKeys: HexString[];\n /** Minimum number of signatures required */\n minSigners: number;\n};\n\n/**\n * Represents multisig signer membership and collected signatures.\n */\nexport class Multisig {\n private signatures: SignatureAndPubKey[];\n private minSigners: number;\n private unusedPubKeys: Set<HexString>;\n\n constructor({ signatures = [], unusedPubKeys, minSigners }: MultisigParams) {\n const normalizedSignatures = signatures.map(normalizeSignatureAndPubKey);\n const normalizedUnusedPubKeys = unusedPubKeys.map((pubKey) =>\n normalizeMultisigHex(\"public key\", pubKey),\n );\n\n assertValidSignerSet(\n normalizedSignatures,\n normalizedUnusedPubKeys,\n minSigners,\n );\n\n this.signatures = normalizedSignatures;\n this.unusedPubKeys = new Set(normalizedUnusedPubKeys);\n this.minSigners = minSigners;\n }\n\n static fromPubKeys(allPubKeys: HexString[], minSigners: number): Multisig {\n return new Multisig({\n signatures: [],\n unusedPubKeys: allPubKeys,\n minSigners,\n });\n }\n\n addSignature(signature: HexString, pubKey: HexString): void {\n const pair = normalizeSignatureAndPubKey({ signature, pub_key: pubKey });\n\n if (!this.unusedPubKeys.delete(pair.pub_key)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pair.pub_key}`,\n );\n }\n\n this.signatures.push(pair);\n }\n\n get isComplete(): boolean {\n return this.signatures.length >= this.minSigners;\n }\n\n get threshold(): number {\n return this.minSigners;\n }\n\n get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]> {\n return this.signatures.map((pair) => ({ ...pair }));\n }\n\n get remainingPubKeys(): Readonly<Set<HexString>> {\n return new Set(this.unusedPubKeys);\n }\n\n get allPubKeys(): Readonly<HexString[]> {\n return [\n ...this.signatures.map((signature) => signature.pub_key),\n ...this.unusedPubKeys,\n ];\n }\n\n toTransaction<RuntimeCall>(\n unsignedTx: UnsignedTransaction<RuntimeCall>,\n ): TransactionV1<RuntimeCall> {\n if (!this.isComplete) {\n throw new MultisigError(\"Multisig transaction is incomplete\");\n }\n\n return {\n V1: {\n ...unsignedTx,\n signatures: [...this.signaturesAndPubKeys],\n unused_pub_keys: [...this.remainingPubKeys],\n min_signers: this.threshold,\n },\n };\n }\n\n getMultisigAddress(hasher: \"sha256\" = \"sha256\"): Uint8Array {\n if (hasher !== \"sha256\") {\n throw new Error(`Unsupported hasher: ${hasher}`);\n }\n\n const sortedPubKeys = [...this.allPubKeys].sort();\n const pubKeyBytes = sortedPubKeys.map((pubKey) =>\n Array.from(hexToBytes(pubKey)),\n );\n const buffer = new borsh.BinaryWriter();\n\n buffer.writeU8(this.minSigners);\n buffer.writeArray(pubKeyBytes, (pkBytes: number[]) => {\n buffer.writeFixedArray(new Uint8Array(pkBytes));\n });\n\n return sha256(buffer.toArray());\n }\n}\n\nfunction assertValidSignerSet(\n signatures: SignatureAndPubKey[],\n unusedPubKeys: HexString[],\n minSigners: number,\n): void {\n // `signatures` and `unusedPubKeys` are expected to already be normalized and\n // validated hex strings by the time this runs.\n const seenPubKeys = new Set<HexString>();\n const allPubKeys = [\n ...signatures.map((signature) => signature.pub_key),\n ...unusedPubKeys,\n ];\n\n if (!Number.isInteger(minSigners) || minSigners < 1) {\n throw new InvalidMultisigParameterError(\n `minSigners must be a positive integer, got ${minSigners}`,\n );\n }\n\n if (allPubKeys.length < 2 || allPubKeys.length > MAX_SIGNERS) {\n throw new InvalidMultisigParameterError(\n `expected 2-${MAX_SIGNERS} total signers, got ${allPubKeys.length}`,\n );\n }\n\n if (minSigners > allPubKeys.length) {\n throw new InvalidMultisigParameterError(\n `minSigners cannot exceed total signer count: ${minSigners} > ${allPubKeys.length}`,\n );\n }\n\n for (const pubKey of allPubKeys) {\n if (seenPubKeys.has(pubKey)) {\n throw new InvalidMultisigParameterError(\n `Duplicate multisig public key: ${pubKey}`,\n );\n }\n\n seenPubKeys.add(pubKey);\n }\n}\n\nfunction normalizeSignatureAndPubKey(\n pair: SignatureAndPubKey,\n): SignatureAndPubKey {\n return {\n signature: normalizeMultisigHex(\"signature\", pair.signature),\n pub_key: normalizeMultisigHex(\"public key\", pair.pub_key),\n };\n}\n\nfunction normalizeMultisigHex(kind: string, value: HexString): HexString {\n try {\n return normalizeHexString(value);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new InvalidMultisigParameterError(\n `Invalid multisig ${kind} ${value}: ${message}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAuB;AAOvB,mBAA+C;AAC/C,YAAuB;AAEhB,IAAM,cAAc;AAKpB,IAAM,gBAAN,cAA4B,MAAM;AAAC;AAKnC,IAAM,gCAAN,cAA4C,cAAc;AAAA,EAC/D,YAAY,MAAc;AACxB,UAAM,oDAAoD,IAAI,EAAE;AAAA,EAClE;AACF;AAiBO,IAAM,WAAN,MAAM,UAAS;AAAA,EAKpB,YAAY,EAAE,aAAa,CAAC,GAAG,eAAe,WAAW,GAAmB;AAC1E,UAAM,uBAAuB,WAAW,IAAI,2BAA2B;AACvE,UAAM,0BAA0B,cAAc;AAAA,MAAI,CAAC,WACjD,qBAAqB,cAAc,MAAM;AAAA,IAC3C;AAEA;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,SAAK,aAAa;AAClB,SAAK,gBAAgB,IAAI,IAAI,uBAAuB;AACpD,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAO,YAAY,YAAyB,YAA8B;AACxE,WAAO,IAAI,UAAS;AAAA,MAClB,YAAY,CAAC;AAAA,MACb,eAAe;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,aAAa,WAAsB,QAAyB;AAC1D,UAAM,OAAO,4BAA4B,EAAE,WAAW,SAAS,OAAO,CAAC;AAEvE,QAAI,CAAC,KAAK,cAAc,OAAO,KAAK,OAAO,GAAG;AAC5C,YAAM,IAAI;AAAA,QACR,qEAAqE,KAAK,OAAO;AAAA,MACnF;AAAA,IACF;AAEA,SAAK,WAAW,KAAK,IAAI;AAAA,EAC3B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK,WAAW,UAAU,KAAK;AAAA,EACxC;AAAA,EAEA,IAAI,YAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,uBAAuD;AACzD,WAAO,KAAK,WAAW,IAAI,CAAC,UAAU,EAAE,GAAG,KAAK,EAAE;AAAA,EACpD;AAAA,EAEA,IAAI,mBAA6C;AAC/C,WAAO,IAAI,IAAI,KAAK,aAAa;AAAA,EACnC;AAAA,EAEA,IAAI,aAAoC;AACtC,WAAO;AAAA,MACL,GAAG,KAAK,WAAW,IAAI,CAAC,cAAc,UAAU,OAAO;AAAA,MACvD,GAAG,KAAK;AAAA,IACV;AAAA,EACF;AAAA,EAEA,cACE,YAC4B;AAC5B,QAAI,CAAC,KAAK,YAAY;AACpB,YAAM,IAAI,cAAc,oCAAoC;AAAA,IAC9D;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,QACF,GAAG;AAAA,QACH,YAAY,CAAC,GAAG,KAAK,oBAAoB;AAAA,QACzC,iBAAiB,CAAC,GAAG,KAAK,gBAAgB;AAAA,QAC1C,aAAa,KAAK;AAAA,MACpB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,mBAAmB,SAAmB,UAAsB;AAC1D,QAAI,WAAW,UAAU;AACvB,YAAM,IAAI,MAAM,uBAAuB,MAAM,EAAE;AAAA,IACjD;AAEA,UAAM,gBAAgB,CAAC,GAAG,KAAK,UAAU,EAAE,KAAK;AAChD,UAAM,cAAc,cAAc;AAAA,MAAI,CAAC,WACrC,MAAM,SAAK,yBAAW,MAAM,CAAC;AAAA,IAC/B;AACA,UAAM,SAAS,IAAU,mBAAa;AAEtC,WAAO,QAAQ,KAAK,UAAU;AAC9B,WAAO,WAAW,aAAa,CAAC,YAAsB;AACpD,aAAO,gBAAgB,IAAI,WAAW,OAAO,CAAC;AAAA,IAChD,CAAC;AAED,eAAO,oBAAO,OAAO,QAAQ,CAAC;AAAA,EAChC;AACF;AAEA,SAAS,qBACP,YACA,eACA,YACM;AAGN,QAAM,cAAc,oBAAI,IAAe;AACvC,QAAM,aAAa;AAAA,IACjB,GAAG,WAAW,IAAI,CAAC,cAAc,UAAU,OAAO;AAAA,IAClD,GAAG;AAAA,EACL;AAEA,MAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GAAG;AACnD,UAAM,IAAI;AAAA,MACR,8CAA8C,UAAU;AAAA,IAC1D;AAAA,EACF;AAEA,MAAI,WAAW,SAAS,KAAK,WAAW,SAAS,aAAa;AAC5D,UAAM,IAAI;AAAA,MACR,cAAc,WAAW,uBAAuB,WAAW,MAAM;AAAA,IACnE;AAAA,EACF;AAEA,MAAI,aAAa,WAAW,QAAQ;AAClC,UAAM,IAAI;AAAA,MACR,gDAAgD,UAAU,MAAM,WAAW,MAAM;AAAA,IACnF;AAAA,EACF;AAEA,aAAW,UAAU,YAAY;AAC/B,QAAI,YAAY,IAAI,MAAM,GAAG;AAC3B,YAAM,IAAI;AAAA,QACR,kCAAkC,MAAM;AAAA,MAC1C;AAAA,IACF;AAEA,gBAAY,IAAI,MAAM;AAAA,EACxB;AACF;AAEA,SAAS,4BACP,MACoB;AACpB,SAAO;AAAA,IACL,WAAW,qBAAqB,aAAa,KAAK,SAAS;AAAA,IAC3D,SAAS,qBAAqB,cAAc,KAAK,OAAO;AAAA,EAC1D;AACF;AAEA,SAAS,qBAAqB,MAAc,OAA6B;AACvE,MAAI;AACF,eAAO,iCAAmB,KAAK;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,UAAM,IAAI;AAAA,MACR,oBAAoB,IAAI,IAAI,KAAK,KAAK,OAAO;AAAA,IAC/C;AAAA,EACF;AACF;","names":[]}
package/dist/index.mjs CHANGED
@@ -1,170 +1,85 @@
1
1
  // src/index.ts
2
2
  import { sha256 } from "@noble/hashes/sha2";
3
- import { hexToBytes } from "@sovereign-sdk/utils";
3
+ import { hexToBytes, normalizeHexString } from "@sovereign-sdk/utils";
4
4
  import * as borsh from "borsh";
5
+ var MAX_SIGNERS = 21;
5
6
  var MultisigError = class extends Error {
6
7
  };
7
- var InvalidTransactionVersionError = class extends MultisigError {
8
- };
9
- var TransactionMismatchError = class extends Error {
10
- constructor() {
11
- super(
12
- "Provided transaction does not match the expected unsigned transaction"
13
- );
14
- }
15
- };
16
8
  var InvalidMultisigParameterError = class extends MultisigError {
17
9
  constructor(desc) {
18
10
  super(`Multisig was constructed with invalid parameter: ${desc}`);
19
11
  }
20
12
  };
21
- var MultisigTransaction = class _MultisigTransaction {
22
- /**
23
- * Creates a new MultisigTransaction instance.
24
- * @param params - The multisig parameters including unsigned transaction, signatures, and public keys
25
- */
26
- constructor({
27
- unsignedTx,
28
- signatures,
29
- unusedPubKeys,
30
- minSigners
31
- }) {
32
- this.signatures = [];
33
- this.unusedPubKeys = /* @__PURE__ */ new Set();
34
- this.unsignedTx = unsignedTx;
35
- this.signatures = signatures;
36
- this.unusedPubKeys = new Set(unusedPubKeys);
13
+ var Multisig = class _Multisig {
14
+ constructor({ signatures = [], unusedPubKeys, minSigners }) {
15
+ const normalizedSignatures = signatures.map(normalizeSignatureAndPubKey);
16
+ const normalizedUnusedPubKeys = unusedPubKeys.map(
17
+ (pubKey) => normalizeMultisigHex("public key", pubKey)
18
+ );
19
+ assertValidSignerSet(
20
+ normalizedSignatures,
21
+ normalizedUnusedPubKeys,
22
+ minSigners
23
+ );
24
+ this.signatures = normalizedSignatures;
25
+ this.unusedPubKeys = new Set(normalizedUnusedPubKeys);
37
26
  this.minSigners = minSigners;
38
27
  }
39
- /**
40
- * Creates an empty MultisigTransaction with no signatures collected yet.
41
- * @param unsignedTx - The unsigned transaction to be signed by multiple parties
42
- * @param minSigners - Minimum number of signatures required for completion
43
- * @param allPubKeys - All public keys that are members of the multisig
44
- * @returns A new empty MultisigTransaction instance
45
- */
46
- static empty(unsignedTx, minSigners, allPubKeys) {
47
- return new _MultisigTransaction({
48
- unsignedTx,
28
+ static fromPubKeys(allPubKeys, minSigners) {
29
+ return new _Multisig({
49
30
  signatures: [],
50
31
  unusedPubKeys: allPubKeys,
51
32
  minSigners
52
33
  });
53
34
  }
54
- /**
55
- * Creates a MultisigTransaction from an array of already-signed transactions.
56
- * Extracts signatures and validates that all transactions are identical except for signatures.
57
- * @param params - Parameters including the signed transactions, minimum signers, and all public keys
58
- * @returns A new MultisigTransaction instance
59
- * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant
60
- * @throws {TransactionMismatchError} If transactions don't match each other
61
- * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based
62
- */
63
- static fromTransactions({
64
- txns,
65
- minSigners,
66
- allPubKeys
67
- }) {
68
- const signatures = [];
69
- const unsignedTx = asUnsignedTransaction(txns[0]);
70
- const unusedPubKeys = new Set(allPubKeys);
71
- assertIsNonceBasedTx(unsignedTx);
72
- for (const tx of txns) {
73
- assertTxVariant(tx);
74
- assertTxMatchesUnsignedTx(tx, unsignedTx);
75
- const { pub_key, signature } = tx.V0;
76
- if (!unusedPubKeys.delete(pub_key)) {
77
- throw new InvalidMultisigParameterError(
78
- `Public key is not a member of the multisig or has already signed: ${pub_key}`
79
- );
80
- }
81
- signatures.push({ pub_key, signature });
82
- }
83
- return new _MultisigTransaction({
84
- unsignedTx,
85
- signatures,
86
- unusedPubKeys: Array.from(unusedPubKeys),
87
- minSigners
88
- });
89
- }
90
- /**
91
- * Adds a signature & public key from a signed transaction to the multisig transaction.
92
- * @param tx - The signed transaction to extract signature from
93
- * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant
94
- * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction
95
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
96
- */
97
- addSignedTransaction(tx) {
98
- assertTxVariant(tx);
99
- assertTxMatchesUnsignedTx(tx, this.unsignedTx);
100
- const { pub_key, signature } = tx.V0;
101
- this.addSignature(signature, pub_key);
102
- }
103
- /**
104
- * Adds a signature to the multisig transaction.
105
- * @param signature - The signature to add
106
- * @param pubKey - The public key corresponding to the signature
107
- * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed
108
- */
109
35
  addSignature(signature, pubKey) {
110
- if (!this.unusedPubKeys.delete(pubKey)) {
36
+ const pair = normalizeSignatureAndPubKey({ signature, pub_key: pubKey });
37
+ if (!this.unusedPubKeys.delete(pair.pub_key)) {
111
38
  throw new InvalidMultisigParameterError(
112
- `Public key is not a member of the multisig or has already signed: ${pubKey}`
39
+ `Public key is not a member of the multisig or has already signed: ${pair.pub_key}`
113
40
  );
114
41
  }
115
- this.signatures.push({ pub_key: pubKey, signature });
42
+ this.signatures.push(pair);
116
43
  }
117
- /**
118
- * Checks if the multisig has collected enough signatures to be complete.
119
- * @returns True if the number of signatures meets or exceeds the minimum required
120
- */
121
44
  get isComplete() {
122
45
  return this.signatures.length >= this.minSigners;
123
46
  }
124
- /**
125
- * Gets the unsigned transaction.
126
- * @returns A readonly view of the unsigned transaction
127
- */
128
- get unsignedTransaction() {
129
- return this.unsignedTx;
130
- }
131
- /**
132
- * Gets the signatures and public keys collected so far.
133
- * @returns A readonly view of the signatures and public keys
134
- */
47
+ get threshold() {
48
+ return this.minSigners;
49
+ }
135
50
  get signaturesAndPubKeys() {
136
- return this.signatures;
51
+ return this.signatures.map((pair) => ({ ...pair }));
137
52
  }
138
- /**
139
- * Gets the remaining public keys that haven't signed yet.
140
- * @returns A readonly view of the remaining public keys
141
- */
142
53
  get remainingPubKeys() {
143
- return this.unusedPubKeys;
54
+ return new Set(this.unusedPubKeys);
144
55
  }
145
- /**
146
- * Gets the threshold number of signatures required for the multisig to be complete.
147
- * @returns The threshold number of signatures
148
- */
149
- get threshold() {
150
- return this.minSigners;
56
+ get allPubKeys() {
57
+ return [
58
+ ...this.signatures.map((signature) => signature.pub_key),
59
+ ...this.unusedPubKeys
60
+ ];
61
+ }
62
+ toTransaction(unsignedTx) {
63
+ if (!this.isComplete) {
64
+ throw new MultisigError("Multisig transaction is incomplete");
65
+ }
66
+ return {
67
+ V1: {
68
+ ...unsignedTx,
69
+ signatures: [...this.signaturesAndPubKeys],
70
+ unused_pub_keys: [...this.remainingPubKeys],
71
+ min_signers: this.threshold
72
+ }
73
+ };
151
74
  }
152
- /**
153
- * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.
154
- * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))
155
- * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)
156
- * @returns The 32-byte multisig address as a Uint8Array
157
- */
158
75
  getMultisigAddress(hasher = "sha256") {
159
76
  if (hasher !== "sha256") {
160
77
  throw new Error(`Unsupported hasher: ${hasher}`);
161
78
  }
162
- const allPubKeys = [
163
- ...this.signatures.map((s) => s.pub_key),
164
- ...Array.from(this.unusedPubKeys)
165
- ];
166
- const sortedPubKeys = allPubKeys.sort();
167
- const pubKeyBytes = sortedPubKeys.map((pk) => Array.from(hexToBytes(pk)));
79
+ const sortedPubKeys = [...this.allPubKeys].sort();
80
+ const pubKeyBytes = sortedPubKeys.map(
81
+ (pubKey) => Array.from(hexToBytes(pubKey))
82
+ );
168
83
  const buffer = new borsh.BinaryWriter();
169
84
  buffer.writeU8(this.minSigners);
170
85
  buffer.writeArray(pubKeyBytes, (pkBytes) => {
@@ -172,67 +87,57 @@ var MultisigTransaction = class _MultisigTransaction {
172
87
  });
173
88
  return sha256(buffer.toArray());
174
89
  }
175
- /**
176
- * Converts the multisig to a V1 transaction that can be submitted to the network.
177
- * @returns A V1 transaction containing all collected signatures and unused public keys
178
- */
179
- asTransaction() {
180
- return {
181
- V1: {
182
- runtime_call: this.unsignedTx.runtime_call,
183
- uniqueness: this.unsignedTx.uniqueness,
184
- details: this.unsignedTx.details,
185
- unused_pub_keys: Array.from(this.unusedPubKeys),
186
- signatures: this.signatures,
187
- min_signers: this.minSigners
188
- }
189
- };
190
- }
191
90
  };
192
- function assertTxVariant(tx) {
193
- if ("V1" in tx) {
194
- throw new InvalidTransactionVersionError(
195
- "Input transaction was an unsupported variant (V1)"
91
+ function assertValidSignerSet(signatures, unusedPubKeys, minSigners) {
92
+ const seenPubKeys = /* @__PURE__ */ new Set();
93
+ const allPubKeys = [
94
+ ...signatures.map((signature) => signature.pub_key),
95
+ ...unusedPubKeys
96
+ ];
97
+ if (!Number.isInteger(minSigners) || minSigners < 1) {
98
+ throw new InvalidMultisigParameterError(
99
+ `minSigners must be a positive integer, got ${minSigners}`
196
100
  );
197
101
  }
198
- }
199
- function asUnsignedTransaction(tx) {
200
- if ("V0" in tx) {
201
- return {
202
- runtime_call: tx.V0.runtime_call,
203
- uniqueness: tx.V0.uniqueness,
204
- details: tx.V0.details
205
- };
102
+ if (allPubKeys.length < 2 || allPubKeys.length > MAX_SIGNERS) {
103
+ throw new InvalidMultisigParameterError(
104
+ `expected 2-${MAX_SIGNERS} total signers, got ${allPubKeys.length}`
105
+ );
206
106
  }
207
- if ("V1" in tx) {
208
- return {
209
- runtime_call: tx.V1.runtime_call,
210
- uniqueness: tx.V1.uniqueness,
211
- details: tx.V1.details
212
- };
107
+ if (minSigners > allPubKeys.length) {
108
+ throw new InvalidMultisigParameterError(
109
+ `minSigners cannot exceed total signer count: ${minSigners} > ${allPubKeys.length}`
110
+ );
213
111
  }
214
- throw new InvalidTransactionVersionError(
215
- "Transaction variant is neither V0 nor V1"
216
- );
217
- }
218
- function assertTxMatchesUnsignedTx(tx, unsignedTx) {
219
- const txAsUnsigned = asUnsignedTransaction(tx);
220
- if (JSON.stringify(txAsUnsigned) !== JSON.stringify(unsignedTx)) {
221
- throw new TransactionMismatchError();
112
+ for (const pubKey of allPubKeys) {
113
+ if (seenPubKeys.has(pubKey)) {
114
+ throw new InvalidMultisigParameterError(
115
+ `Duplicate multisig public key: ${pubKey}`
116
+ );
117
+ }
118
+ seenPubKeys.add(pubKey);
222
119
  }
223
120
  }
224
- function assertIsNonceBasedTx(unsignedTx) {
225
- if (!("nonce" in unsignedTx.uniqueness)) {
121
+ function normalizeSignatureAndPubKey(pair) {
122
+ return {
123
+ signature: normalizeMultisigHex("signature", pair.signature),
124
+ pub_key: normalizeMultisigHex("public key", pair.pub_key)
125
+ };
126
+ }
127
+ function normalizeMultisigHex(kind, value) {
128
+ try {
129
+ return normalizeHexString(value);
130
+ } catch (error) {
131
+ const message = error instanceof Error ? error.message : String(error);
226
132
  throw new InvalidMultisigParameterError(
227
- "Only nonce-based transactions are supported for multisig"
133
+ `Invalid multisig ${kind} ${value}: ${message}`
228
134
  );
229
135
  }
230
136
  }
231
137
  export {
232
138
  InvalidMultisigParameterError,
233
- InvalidTransactionVersionError,
234
- MultisigError,
235
- MultisigTransaction,
236
- TransactionMismatchError
139
+ MAX_SIGNERS,
140
+ Multisig,
141
+ MultisigError
237
142
  };
238
143
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2\";\nimport type {\n SignatureAndPubKey,\n Transaction,\n TransactionV0,\n UnsignedTransaction,\n} from \"@sovereign-sdk/types\";\nimport type { HexString } from \"@sovereign-sdk/utils\";\nimport { bytesToHex, hexToBytes } from \"@sovereign-sdk/utils\";\nimport * as borsh from \"borsh\";\n\n/**\n * Base error class for multisig-related errors.\n */\nexport class MultisigError extends Error {}\n\n/**\n * Error thrown when a transaction has an unsupported version for multisig operations.\n */\nexport class InvalidTransactionVersionError extends MultisigError {}\n\n/**\n * Error thrown when a provided transaction doesn't match the expected unsigned transaction.\n */\nexport class TransactionMismatchError extends Error {\n constructor() {\n super(\n \"Provided transaction does not match the expected unsigned transaction\",\n );\n }\n}\n\n/**\n * Error thrown when multisig is constructed or used with invalid parameters.\n */\nexport class InvalidMultisigParameterError extends MultisigError {\n constructor(desc: string) {\n super(`Multisig was constructed with invalid parameter: ${desc}`);\n }\n}\n\n/**\n * Parameters for constructing a MultisigTransaction.\n */\nexport type MultisigParams = {\n /** The unsigned transaction to be signed by multiple parties */\n unsignedTx: UnsignedTransaction<unknown>;\n /** Array of signatures with pubkeys already collected */\n signatures: SignatureAndPubKey[];\n /** Array of public keys that haven't signed yet */\n unusedPubKeys: HexString[];\n /** Minimum number of signatures required */\n minSigners: number;\n};\n\n/**\n * Parameters for creating a MultisigTransaction from existing signed transactions.\n */\nexport type FromTransactionsParams = {\n /** Array of signed transactions to extract signatures from */\n txns: Transaction<unknown>[];\n /** Minimum number of signatures required */\n minSigners: number;\n /** All public keys that are members of the multisig */\n allPubKeys: HexString[];\n};\n\n/**\n * Represents a multisig transaction that collects signatures from multiple parties.\n * Only supports nonce-based transactions and V0 transaction variants for signature collection.\n */\nexport class MultisigTransaction {\n private unsignedTx: UnsignedTransaction<unknown>;\n private signatures: SignatureAndPubKey[] = [];\n private minSigners: number;\n private unusedPubKeys = new Set<HexString>();\n\n /**\n * Creates a new MultisigTransaction instance.\n * @param params - The multisig parameters including unsigned transaction, signatures, and public keys\n */\n constructor({\n unsignedTx,\n signatures,\n unusedPubKeys,\n minSigners,\n }: MultisigParams) {\n this.unsignedTx = unsignedTx;\n this.signatures = signatures;\n this.unusedPubKeys = new Set(unusedPubKeys);\n this.minSigners = minSigners;\n }\n\n /**\n * Creates an empty MultisigTransaction with no signatures collected yet.\n * @param unsignedTx - The unsigned transaction to be signed by multiple parties\n * @param minSigners - Minimum number of signatures required for completion\n * @param allPubKeys - All public keys that are members of the multisig\n * @returns A new empty MultisigTransaction instance\n */\n static empty(\n unsignedTx: UnsignedTransaction<unknown>,\n minSigners: number,\n allPubKeys: HexString[],\n ): MultisigTransaction {\n return new MultisigTransaction({\n unsignedTx,\n signatures: [],\n unusedPubKeys: allPubKeys,\n minSigners,\n });\n }\n\n /**\n * Creates a MultisigTransaction from an array of already-signed transactions.\n * Extracts signatures and validates that all transactions are identical except for signatures.\n * @param params - Parameters including the signed transactions, minimum signers, and all public keys\n * @returns A new MultisigTransaction instance\n * @throws {InvalidTransactionVersionError} If any transaction is not V0 variant\n * @throws {TransactionMismatchError} If transactions don't match each other\n * @throws {InvalidMultisigParameterError} If public keys are invalid or transaction is not nonce-based\n */\n static fromTransactions({\n txns,\n minSigners,\n allPubKeys,\n }: FromTransactionsParams): MultisigTransaction {\n const signatures: SignatureAndPubKey[] = [];\n const unsignedTx = asUnsignedTransaction(txns[0]);\n const unusedPubKeys = new Set<HexString>(allPubKeys);\n\n assertIsNonceBasedTx(unsignedTx);\n\n for (const tx of txns) {\n assertTxVariant(tx);\n assertTxMatchesUnsignedTx(tx, unsignedTx);\n\n const { pub_key, signature } = tx.V0;\n\n if (!unusedPubKeys.delete(pub_key)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pub_key}`,\n );\n }\n\n signatures.push({ pub_key, signature });\n }\n\n return new MultisigTransaction({\n unsignedTx,\n signatures,\n unusedPubKeys: Array.from(unusedPubKeys),\n minSigners,\n });\n }\n\n /**\n * Adds a signature & public key from a signed transaction to the multisig transaction.\n * @param tx - The signed transaction to extract signature from\n * @throws {InvalidTransactionVersionError} If the transaction is not V0 variant\n * @throws {TransactionMismatchError} If the transaction doesn't match the unsigned transaction\n * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed\n */\n addSignedTransaction(tx: Transaction<unknown>): void {\n assertTxVariant(tx);\n assertTxMatchesUnsignedTx(tx, this.unsignedTx);\n\n const { pub_key, signature } = tx.V0;\n\n this.addSignature(signature, pub_key);\n }\n\n /**\n * Adds a signature to the multisig transaction.\n * @param signature - The signature to add\n * @param pubKey - The public key corresponding to the signature\n * @throws {InvalidMultisigParameterError} If the public key is not a member or has already signed\n */\n addSignature(signature: HexString, pubKey: HexString): void {\n if (!this.unusedPubKeys.delete(pubKey)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pubKey}`,\n );\n }\n\n this.signatures.push({ pub_key: pubKey, signature });\n }\n\n /**\n * Checks if the multisig has collected enough signatures to be complete.\n * @returns True if the number of signatures meets or exceeds the minimum required\n */\n get isComplete(): boolean {\n return this.signatures.length >= this.minSigners;\n }\n\n /**\n * Gets the unsigned transaction.\n * @returns A readonly view of the unsigned transaction\n */\n get unsignedTransaction(): Readonly<UnsignedTransaction<unknown>> {\n return this.unsignedTx;\n }\n\n /**\n * Gets the signatures and public keys collected so far.\n * @returns A readonly view of the signatures and public keys\n */\n get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]> {\n return this.signatures;\n }\n\n /**\n * Gets the remaining public keys that haven't signed yet.\n * @returns A readonly view of the remaining public keys\n */\n get remainingPubKeys(): Readonly<Set<HexString>> {\n return this.unusedPubKeys;\n }\n\n /**\n * Gets the threshold number of signatures required for the multisig to be complete.\n * @returns The threshold number of signatures\n */\n get threshold(): number {\n return this.minSigners;\n }\n\n /**\n * Calculates the multisig address (credential ID) by hashing the threshold and sorted public keys.\n * This matches the Rust implementation: hash(threshold || borsh(sorted_pubkeys))\n * @param hasher - The hash algorithm to use (currently only 'sha256' is supported)\n * @returns The 32-byte multisig address as a Uint8Array\n */\n getMultisigAddress(hasher: \"sha256\" = \"sha256\"): Uint8Array {\n if (hasher !== \"sha256\") {\n throw new Error(`Unsupported hasher: ${hasher}`);\n }\n\n // Collect all public keys (signatures + unused)\n const allPubKeys = [\n ...this.signatures.map((s) => s.pub_key),\n ...Array.from(this.unusedPubKeys),\n ];\n const sortedPubKeys = allPubKeys.sort();\n const pubKeyBytes = sortedPubKeys.map((pk) => Array.from(hexToBytes(pk)));\n const buffer = new borsh.BinaryWriter();\n\n buffer.writeU8(this.minSigners);\n buffer.writeArray(pubKeyBytes, (pkBytes: number[]) => {\n buffer.writeFixedArray(new Uint8Array(pkBytes));\n });\n\n return sha256(buffer.toArray());\n }\n\n /**\n * Converts the multisig to a V1 transaction that can be submitted to the network.\n * @returns A V1 transaction containing all collected signatures and unused public keys\n */\n asTransaction(): Transaction<unknown> {\n return {\n V1: {\n runtime_call: this.unsignedTx.runtime_call,\n uniqueness: this.unsignedTx.uniqueness,\n details: this.unsignedTx.details,\n unused_pub_keys: Array.from(this.unusedPubKeys),\n signatures: this.signatures,\n min_signers: this.minSigners,\n },\n };\n }\n}\n\nfunction assertTxVariant(\n tx: Transaction<unknown>,\n): asserts tx is TransactionV0<unknown> {\n if (\"V1\" in tx) {\n throw new InvalidTransactionVersionError(\n \"Input transaction was an unsupported variant (V1)\",\n );\n }\n}\n\nfunction asUnsignedTransaction(\n tx: Transaction<unknown>,\n): UnsignedTransaction<unknown> {\n if (\"V0\" in tx) {\n return {\n runtime_call: tx.V0.runtime_call,\n uniqueness: tx.V0.uniqueness,\n details: tx.V0.details,\n };\n }\n\n if (\"V1\" in tx) {\n return {\n runtime_call: tx.V1.runtime_call,\n uniqueness: tx.V1.uniqueness,\n details: tx.V1.details,\n };\n }\n\n throw new InvalidTransactionVersionError(\n \"Transaction variant is neither V0 nor V1\",\n );\n}\n\nfunction assertTxMatchesUnsignedTx(\n tx: Transaction<unknown>,\n unsignedTx: UnsignedTransaction<unknown>,\n): void {\n const txAsUnsigned = asUnsignedTransaction(tx);\n\n if (JSON.stringify(txAsUnsigned) !== JSON.stringify(unsignedTx)) {\n throw new TransactionMismatchError();\n }\n}\n\nfunction assertIsNonceBasedTx(unsignedTx: UnsignedTransaction<unknown>): void {\n if (!(\"nonce\" in unsignedTx.uniqueness)) {\n throw new InvalidMultisigParameterError(\n \"Only nonce-based transactions are supported for multisig\",\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { sha256 } from \"@noble/hashes/sha2\";\nimport type {\n SignatureAndPubKey,\n TransactionV1,\n UnsignedTransaction,\n} from \"@sovereign-sdk/types\";\nimport type { HexString } from \"@sovereign-sdk/utils\";\nimport { hexToBytes, normalizeHexString } from \"@sovereign-sdk/utils\";\nimport * as borsh from \"borsh\";\n\nexport const MAX_SIGNERS = 21;\n\n/**\n * Base error class for multisig-related errors.\n */\nexport class MultisigError extends Error {}\n\n/**\n * Error thrown when multisig is constructed or used with invalid parameters.\n */\nexport class InvalidMultisigParameterError extends MultisigError {\n constructor(desc: string) {\n super(`Multisig was constructed with invalid parameter: ${desc}`);\n }\n}\n\n/**\n * Parameters for constructing a Multisig.\n */\nexport type MultisigParams = {\n /** Array of signatures with pubkeys already collected */\n signatures?: SignatureAndPubKey[];\n /** Array of public keys that haven't signed yet */\n unusedPubKeys: HexString[];\n /** Minimum number of signatures required */\n minSigners: number;\n};\n\n/**\n * Represents multisig signer membership and collected signatures.\n */\nexport class Multisig {\n private signatures: SignatureAndPubKey[];\n private minSigners: number;\n private unusedPubKeys: Set<HexString>;\n\n constructor({ signatures = [], unusedPubKeys, minSigners }: MultisigParams) {\n const normalizedSignatures = signatures.map(normalizeSignatureAndPubKey);\n const normalizedUnusedPubKeys = unusedPubKeys.map((pubKey) =>\n normalizeMultisigHex(\"public key\", pubKey),\n );\n\n assertValidSignerSet(\n normalizedSignatures,\n normalizedUnusedPubKeys,\n minSigners,\n );\n\n this.signatures = normalizedSignatures;\n this.unusedPubKeys = new Set(normalizedUnusedPubKeys);\n this.minSigners = minSigners;\n }\n\n static fromPubKeys(allPubKeys: HexString[], minSigners: number): Multisig {\n return new Multisig({\n signatures: [],\n unusedPubKeys: allPubKeys,\n minSigners,\n });\n }\n\n addSignature(signature: HexString, pubKey: HexString): void {\n const pair = normalizeSignatureAndPubKey({ signature, pub_key: pubKey });\n\n if (!this.unusedPubKeys.delete(pair.pub_key)) {\n throw new InvalidMultisigParameterError(\n `Public key is not a member of the multisig or has already signed: ${pair.pub_key}`,\n );\n }\n\n this.signatures.push(pair);\n }\n\n get isComplete(): boolean {\n return this.signatures.length >= this.minSigners;\n }\n\n get threshold(): number {\n return this.minSigners;\n }\n\n get signaturesAndPubKeys(): Readonly<SignatureAndPubKey[]> {\n return this.signatures.map((pair) => ({ ...pair }));\n }\n\n get remainingPubKeys(): Readonly<Set<HexString>> {\n return new Set(this.unusedPubKeys);\n }\n\n get allPubKeys(): Readonly<HexString[]> {\n return [\n ...this.signatures.map((signature) => signature.pub_key),\n ...this.unusedPubKeys,\n ];\n }\n\n toTransaction<RuntimeCall>(\n unsignedTx: UnsignedTransaction<RuntimeCall>,\n ): TransactionV1<RuntimeCall> {\n if (!this.isComplete) {\n throw new MultisigError(\"Multisig transaction is incomplete\");\n }\n\n return {\n V1: {\n ...unsignedTx,\n signatures: [...this.signaturesAndPubKeys],\n unused_pub_keys: [...this.remainingPubKeys],\n min_signers: this.threshold,\n },\n };\n }\n\n getMultisigAddress(hasher: \"sha256\" = \"sha256\"): Uint8Array {\n if (hasher !== \"sha256\") {\n throw new Error(`Unsupported hasher: ${hasher}`);\n }\n\n const sortedPubKeys = [...this.allPubKeys].sort();\n const pubKeyBytes = sortedPubKeys.map((pubKey) =>\n Array.from(hexToBytes(pubKey)),\n );\n const buffer = new borsh.BinaryWriter();\n\n buffer.writeU8(this.minSigners);\n buffer.writeArray(pubKeyBytes, (pkBytes: number[]) => {\n buffer.writeFixedArray(new Uint8Array(pkBytes));\n });\n\n return sha256(buffer.toArray());\n }\n}\n\nfunction assertValidSignerSet(\n signatures: SignatureAndPubKey[],\n unusedPubKeys: HexString[],\n minSigners: number,\n): void {\n // `signatures` and `unusedPubKeys` are expected to already be normalized and\n // validated hex strings by the time this runs.\n const seenPubKeys = new Set<HexString>();\n const allPubKeys = [\n ...signatures.map((signature) => signature.pub_key),\n ...unusedPubKeys,\n ];\n\n if (!Number.isInteger(minSigners) || minSigners < 1) {\n throw new InvalidMultisigParameterError(\n `minSigners must be a positive integer, got ${minSigners}`,\n );\n }\n\n if (allPubKeys.length < 2 || allPubKeys.length > MAX_SIGNERS) {\n throw new InvalidMultisigParameterError(\n `expected 2-${MAX_SIGNERS} total signers, got ${allPubKeys.length}`,\n );\n }\n\n if (minSigners > allPubKeys.length) {\n throw new InvalidMultisigParameterError(\n `minSigners cannot exceed total signer count: ${minSigners} > ${allPubKeys.length}`,\n );\n }\n\n for (const pubKey of allPubKeys) {\n if (seenPubKeys.has(pubKey)) {\n throw new InvalidMultisigParameterError(\n `Duplicate multisig public key: ${pubKey}`,\n );\n }\n\n seenPubKeys.add(pubKey);\n }\n}\n\nfunction normalizeSignatureAndPubKey(\n pair: SignatureAndPubKey,\n): SignatureAndPubKey {\n return {\n signature: normalizeMultisigHex(\"signature\", pair.signature),\n pub_key: normalizeMultisigHex(\"public key\", pair.pub_key),\n };\n}\n\nfunction normalizeMultisigHex(kind: string, value: HexString): HexString {\n try {\n return normalizeHexString(value);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new InvalidMultisigParameterError(\n `Invalid multisig ${kind} ${value}: ${message}`,\n );\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@sovereign-sdk/multisig",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Multisig functionality for Sovereign SDK applications",
5
- "repository": "github:Sovereign-Labs/sovereign-sdk",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Sovereign-Labs/sovereign-sdk.git"
8
+ },
6
9
  "files": [
7
10
  "dist",
8
11
  "README.md",
@@ -22,8 +25,7 @@
22
25
  "tsup": "^8.3.0",
23
26
  "typescript": "^5.6.3",
24
27
  "vitest": "4.0.7",
25
- "@sovereign-sdk/types": "^0.1.2",
26
- "@sovereign-sdk/web3": "^0.12.4"
28
+ "@sovereign-sdk/types": "^0.2.0"
27
29
  },
28
30
  "peerDependencies": {
29
31
  "tsup": "^8.3.0",
@@ -35,8 +37,7 @@
35
37
  "dependencies": {
36
38
  "@noble/hashes": "^1.5.0",
37
39
  "borsh": "0.7.0",
38
- "@sovereign-sdk/signers": "^0.5.2",
39
- "@sovereign-sdk/utils": "^0.0.4"
40
+ "@sovereign-sdk/utils": "^0.1.0"
40
41
  },
41
42
  "scripts": {
42
43
  "build": "tsup",
@@ -44,7 +45,6 @@
44
45
  "fix": "biome check --write",
45
46
  "lint": "biome lint",
46
47
  "test": "vitest run **/*.test.ts",
47
- "test:integration": "vitest run **/*.integration-test.ts",
48
48
  "typecheck": "tsc --noEmit"
49
49
  }
50
50
  }