Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1ee11e8
add interface for multisig account wallet
quocle108 Feb 12, 2026
88f148e
build types
quocle108 Feb 12, 2026
32ffd82
lint
quocle108 Feb 12, 2026
58d3535
add propose and approve message interface for multisig
quocle108 Feb 13, 2026
8773e06
add MultisigInfo object
quocle108 Feb 13, 2026
7f89ab5
fix: IWalletAccountMultisigReadOnly as an interface
jonathunne Feb 14, 2026
95f8f3e
fix: IWalletAccountMultisig as an interface
jonathunne Feb 14, 2026
206d1e9
Update src/wallet-account-multisig-read-only.js
quocle108 Feb 15, 2026
69851ed
Update src/wallet-account-multisig.js
quocle108 Feb 15, 2026
8980e74
Update src/wallet-account-multisig.js
quocle108 Feb 15, 2026
eb43fd4
refactor: update multisig interface hierarchy and naming
quocle108 Feb 16, 2026
3fbf34e
remove redundant file
quocle108 Feb 16, 2026
c9b9fc4
Update src/wallet-account-multisig.js
quocle108 Feb 19, 2026
e062b0c
Update src/wallet-account-multisig.js
quocle108 Feb 19, 2026
5ffcb43
update type and desc
quocle108 Feb 19, 2026
e6de41c
fix: IWalletAccountMultisig extends both IWalletAccount and IWalletAc…
quocle108 Feb 23, 2026
a06073a
feat: override sendTransaction and transfer in IWalletAccountMultisig
quocle108 Feb 23, 2026
44ff471
update types
quocle108 Feb 23, 2026
4ddea45
use interface instead of class in type
quocle108 Feb 23, 2026
ca897ac
update execute multisig type
quocle108 Feb 23, 2026
864fe13
update MultisigTransactionResult type and desc
quocle108 Feb 23, 2026
1411611
revert: requires fee and hash for MultisigTransactionResult
quocle108 Feb 23, 2026
9392415
Update src/wallet-account-multisig.js
quocle108 Feb 24, 2026
4ec73ba
correct desc for hash in MultisigTransactionResult
quocle108 Feb 25, 2026
aa4c57c
update function naming for multisig module
quocle108 Mar 6, 2026
f8bced5
Refactor code.
Davi0kProgramsThings Mar 11, 2026
922dd4d
feat(multisig): add chain-agnostic IMultisigTransport interface
AlonzoRicardo Jun 11, 2026
50b4c9a
refactor(multisig): apply ISP so multisig interfaces don't extend IWa…
AlonzoRicardo Jun 15, 2026
5d00450
refactor(multisig): move signer-key members to the writable interface
AlonzoRicardo Jun 16, 2026
24ec002
refactor(multisig): make IMultisigTransport generic in the payload types
AlonzoRicardo Jun 18, 2026
8a6e212
refactor(multisig): address review — generic fee wording + messageHas…
AlonzoRicardo Jun 18, 2026
eb4b117
feat(multisig): add toTransportJson helper for hand-serializing trans…
AlonzoRicardo Jun 19, 2026
98b1b7b
refactor(multisig): add proposal status enum and unify the propose re…
AlonzoRicardo Jun 19, 2026
bf9d980
refactor(multisig): clean up root type barrel and use the extends key…
AlonzoRicardo Jun 22, 2026
a31604e
refactor(multisig): make the transport read methods generic too
AlonzoRicardo Jun 24, 2026
d1ab5ae
feat(multisig): extend toTransportJson to encode byte arrays as hex
AlonzoRicardo Jun 25, 2026
e99ef9a
feat(multisig): adopt the shared error taxonomy in the interface cont…
AlonzoRicardo Jul 10, 2026
97f30a1
docs(multisig): clarify why MultisigInfo.isCreated is optional
AlonzoRicardo Jul 10, 2026
02554d1
refactor(multisig): align quoteExecuteProposal return with the quote/…
AlonzoRicardo Jul 10, 2026
1a3461f
docs: simplify ValueError description to match the agreed wording
AlonzoRicardo Jul 20, 2026
87d86f0
Merge tetherto/main into add-interface-multisig-account-wallet
AlonzoRicardo Jul 20, 2026
5c8c611
refactor(multisig): make propose the generic surface; drop proposeTra…
AlonzoRicardo Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
"types": "./types/src/protocols/index.d.ts",
"default": "./src/protocols/index.js"
},
"./multisig": {
"types": "./types/src/multisig/index.d.ts",
"default": "./src/multisig/index.js"
},
"./package": {
"default": "./package.json"
}
Expand Down
82 changes: 82 additions & 0 deletions src/multisig/i-multisig-owner-management.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'

import { NotImplementedError } from '../errors.js'

/** @typedef {import('./wallet-account-read-only-multisig.js').MultisigProposal} MultisigProposal */

/** @typedef {import('../errors.js').SignerError} SignerError */

/**
* @typedef {Object} MultisigOptions
* @property {number} threshold - The new amount of approvals required to execute a transaction.
*/

/**
* Optional owner-management surface for multisig accounts whose owner set is mutable
* (e.g. account-abstraction wallets). Chains whose owner set is fixed at creation —
* such as Bitcoin script multisig, where the participants are committed in the redeem
* script — do not implement this interface.
*
* @interface
*/
export class IMultisigOwnerManagement {
/**
* Proposes adding a new owner to the multisig wallet account.
*
* @param {string} owner - The owner's address.
* @param {MultisigOptions} [options] - The multisig options.
* @returns {Promise<MultisigProposal>} The multisig proposal.
* @throws {SignerError} If the signer is not an owner of the multisig account.
*/
async addOwner (owner, options) {
throw new NotImplementedError('addOwner(owner, options)')
}

/**
* Proposes removing an owner from the multisig wallet account.
*
* @param {string} owner - The owner's address.
* @param {MultisigOptions} [options] - The multisig options.
* @returns {Promise<MultisigProposal>} The multisig proposal.
* @throws {SignerError} If the signer is not an owner of the multisig account.
*/
async removeOwner (owner, options) {
throw new NotImplementedError('removeOwner(owner, options)')
}

/**
* Proposes replacing an owner with a different one.
*
* @param {string} oldOwner - The old owner.
* @param {string} newOwner - The new owner.
* @returns {Promise<MultisigProposal>} The multisig proposal.
* @throws {SignerError} If the signer is not an owner of the multisig account.
*/
async swapOwner (oldOwner, newOwner) {
throw new NotImplementedError('swapOwner(oldOwner, newOwner)')
}

/**
* Proposes changing the signature threshold.
*
* @param {number} newThreshold - The new threshold.
* @returns {Promise<MultisigProposal>} The multisig proposal.
* @throws {SignerError} If the signer is not an owner of the multisig account.
*/
async changeThreshold (newThreshold) {
throw new NotImplementedError('changeThreshold(newThreshold)')
}
}
128 changes: 128 additions & 0 deletions src/multisig/i-multisig-transport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'

import { NotImplementedError } from '../errors.js'

/**
* A message proposal to share with the other owners for them to confirm.
*
* @typedef {Object} MultisigTransportMessageInput
* @property {string} message - The message to sign.
* @property {string} signature - The submitting owner's signature over the message.
*/

/**
* A shared transaction proposal returned by the transport. The concrete payload is
* chain-specific and opaque to the transport: an implementation persists whatever the
* chain's execution layer produced and returns it intact, alongside the owner
* confirmations collected so far.
*
* @typedef {Object} MultisigTransportProposal
* @property {unknown[]} confirmations - The owner confirmations (signatures) collected so far.
*/

/**
* A shared message proposal returned by the transport, alongside the owner confirmations
* collected so far. As with proposals, any further fields are chain-specific.
*
* @typedef {Object} MultisigTransportMessage
* @property {unknown[]} confirmations - The owner confirmations (signatures) collected so far.
*/

/**
* Transport for sharing multisig calldata between the owners of a multisig account.
*
* A transport distributes transaction proposals and message proposals (and their
* confirmations) amongst the owners of a multisig account, so that signers running on
* separate machines can coordinate without a shared process. It is chain-agnostic: the
* proposal and message payloads are opaque to the transport and interpreted by each
* chain's multisig package, so the same transport can back account-abstraction, UTXO
* (PSBT) or other multisig wallets. Plug in a custom backend (a hosted service, a
* database, a peer-to-peer channel, etc.) by implementing this interface.
*
* Implementations that serialize the payloads themselves (rather than handing them to an
* SDK that already does it) can pass them through {@link toTransportJson} to convert native
* values such as BigInt into JSON-safe forms before persisting or transmitting them.
*
* @interface
* @template [TProposal=Record<string, unknown>]
* @template [TMessage=MultisigTransportMessageInput]
* @template [TProposalResponse=MultisigTransportProposal]
* @template [TMessageResponse=MultisigTransportMessage]
*/
export class IMultisigTransport {
/**
* Submits a new transaction proposal so the other owners can confirm it.
*
* @param {TProposal} proposal - The signed transaction proposal to share. Opaque to the transport, which must persist it so {@link getProposal} can return it intact.
* @returns {Promise<void>}
*/
async submitProposal (proposal) {
throw new NotImplementedError('submitProposal(proposal)')
}

/**
* Returns a transaction proposal by its identifier.
*
* @param {string} proposalId - The proposal's identifier.
* @returns {Promise<TProposalResponse | null>} The proposal, or null if it has not been found.
*/
async getProposal (proposalId) {
throw new NotImplementedError('getProposal(proposalId)')
}

/**
* Adds an owner's confirmation (signature) to an existing transaction proposal.
*
* @param {string} proposalId - The proposal's identifier.
* @param {string} signature - The owner's signature over the proposal.
* @returns {Promise<void>}
*/
async confirmProposal (proposalId, signature) {
throw new NotImplementedError('confirmProposal(proposalId, signature)')
}

/**
* Submits a new message proposal so the other owners can confirm it.
*
* @param {string} accountAddress - The multisig account's address.
* @param {TMessage} message - The message proposal to share.
* @returns {Promise<void>}
*/
async submitMessage (accountAddress, message) {
throw new NotImplementedError('submitMessage(accountAddress, message)')
}

/**
* Returns a message proposal by its hash.
*
* @param {string} messageId - The message's hash.
* @returns {Promise<TMessageResponse | null>} The message, or null if it has not been found.
*/
async getMessage (messageId) {
throw new NotImplementedError('getMessage(messageId)')
}

/**
* Adds an owner's confirmation (signature) to an existing message proposal.
*
* @param {string} messageId - The message's hash.
* @param {string} signature - The owner's signature over the message.
* @returns {Promise<void>}
*/
async confirmMessage (messageId, signature) {
throw new NotImplementedError('confirmMessage(messageId, signature)')
}
}
37 changes: 37 additions & 0 deletions src/multisig/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'

/** @typedef {import('./wallet-account-read-only-multisig.js').MultisigInfo} MultisigInfo */
/** @typedef {import('./wallet-account-read-only-multisig.js').MultisigProposal} MultisigProposal */
/** @typedef {import('./wallet-account-read-only-multisig.js').MultisigMessage} MultisigMessage */
/** @typedef {import('../wallet-account.js').KeyPair} KeyPair */

/** @typedef {import('./wallet-account-multisig.js').MultisigTransactionOptions} MultisigTransactionOptions */
/** @typedef {import('./wallet-account-multisig.js').MultisigMessageProposal} MultisigMessageProposal */
/** @typedef {import('./i-multisig-owner-management.js').MultisigOptions} MultisigOptions */

/** @typedef {import('./i-multisig-transport.js').MultisigTransportProposal} MultisigTransportProposal */
/** @typedef {import('./i-multisig-transport.js').MultisigTransportMessage} MultisigTransportMessage */
/** @typedef {import('./i-multisig-transport.js').MultisigTransportMessageInput} MultisigTransportMessageInput */

export { IWalletAccountReadOnlyMultisig } from './wallet-account-read-only-multisig.js'

export { IWalletAccountMultisig } from './wallet-account-multisig.js'

export { IMultisigOwnerManagement } from './i-multisig-owner-management.js'

export { IMultisigTransport } from './i-multisig-transport.js'

export { toTransportJson } from './transport-serialization.js'
62 changes: 62 additions & 0 deletions src/multisig/transport-serialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 Tether Operations Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict'

/**
* Converts a byte array to a 0x-prefixed lowercase hex string.
*
* @param {Uint8Array} bytes - The byte array to encode.
* @returns {string} The 0x-prefixed hex encoding.
*/
function bytesToHex (bytes) {
let hex = '0x'
for (const byte of bytes) {
hex += byte.toString(16).padStart(2, '0')
}
return hex
}

/**
* Recursively converts a value into a JSON-safe form so it survives JSON.stringify: every BigInt
* becomes its decimal string and every byte array (Uint8Array, including Buffer) becomes a
* 0x-prefixed lowercase hex string. Arrays and plain objects are converted entry by entry; all
* other values are returned unchanged.
*
* The conversion is one-way: there is no generic inverse, so a consumer that needs the original
* types back restores them per field (it knows which fields are amounts, byte strings, etc.).
*
* @param {unknown} value - The value to convert (object, array, or primitive).
* @returns {unknown} A JSON-safe copy of the value.
*/
export function toTransportJson (value) {
if (typeof value === 'bigint') {
return value.toString()
}

if (value instanceof Uint8Array) {
return bytesToHex(value)
}

if (Array.isArray(value)) {
return value.map(toTransportJson)
}

if (value !== null && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, entry]) => [key, toTransportJson(entry)])
)
}

return value
}
Loading