Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions modules/sdk-coin-xtz/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export interface IndexedSignature extends IndexedData {
export type RecoverOptions = {
userKey: string;
backupKey: string;
bitgoKey?: string;
walletPassphrase?: string;
walletContractAddress: string; // use this as walletBaseAddress for TSS
recoveryDestination: string;
Expand Down
58 changes: 57 additions & 1 deletion modules/sdk-coin-xtz/src/lib/multisigUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as _ from 'lodash';
import { IndexedSignature, OriginationData, OriginationOp, RevealOp, TransactionOp, TransferData } from './iface';
import { DEFAULT_FEE, DEFAULT_GAS_LIMIT, DEFAULT_STORAGE_LIMIT, hashTypes, isValidKey } from './utils';
import {
DEFAULT_FEE,
DEFAULT_GAS_LIMIT,
DEFAULT_STORAGE_LIMIT,
hashTypes,
isValidKey,
isValidPublicKey,
} from './utils';

// Default n of m for multisig wallets
const DEFAULT_N = 2;
Expand Down Expand Up @@ -526,6 +533,55 @@ export function getOwnersPublicKeys(operation: OriginationOp): string[] {
return ownersArgs.map((o) => o.string);
}

/**
* Upper bound on the number of owners we will accept from contract storage. The BitGo generic
* multisig wallet contract has exactly three owners; the bound guards against a compromised or
* misbehaving explorer returning an unbounded list.
*/
const MAX_CONTRACT_OWNERS = 100;

export interface ContractStorage {
ownerKeys: string[];
storedCounter: string;
}

/**
* Extract the owner list and stored counter from the TZKT normalized storage returned by
* `/v1/contracts/{address}/storage`. TZKT returns storage keyed by its Michelson field
* annotations, so the generic multisig storage type
* `pair (nat %stored_counter) (pair (nat %threshold) (list %keys key))` yields a flat
* `{ stored_counter, threshold, keys: ["sppk…", …] }` object whose `keys` array holds base58
* public key strings and `stored_counter` the contract's next counter. Returns undefined for
* any shape that does not match, so the caller fails closed instead of guessing.
*
* @param storage the normalized storage JSON from TZKT
* @returns the owner public keys and stored counter, or undefined if the shape is unexpected
*/
export function extractContractStorage(storage: unknown): ContractStorage | undefined {
if (!storage || typeof storage !== 'object' || Array.isArray(storage)) {
return undefined;
}
if (!('keys' in storage) || !('stored_counter' in storage)) {
return undefined;
}
const keys = storage.keys;
if (!Array.isArray(keys) || keys.length === 0 || keys.length > MAX_CONTRACT_OWNERS) {
return undefined;
}
const ownerKeys: string[] = [];
for (const key of keys) {
if (typeof key !== 'string' || !isValidPublicKey(key)) {
return undefined;
}
ownerKeys.push(key);
}
const storedCounter = storage.stored_counter;
if (typeof storedCounter !== 'string' || !/^\d+$/.test(storedCounter)) {
return undefined;
}
return { ownerKeys, storedCounter };
}

/**
* Generic Multisig contract from https://github.com/murbard/smart-contracts/blob/master/multisig/michelson/generic.tz
*/
Expand Down
19 changes: 19 additions & 0 deletions modules/sdk-coin-xtz/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,25 @@ export class TransactionBuilder extends BaseTransactionBuilder {
return this.transaction;
}

/**
* Register a key pair to sign the multisig contract payload at a given owner index.
* Unlike sign(), this registers contract signers directly and does not route by source
* address, so the backup key (which is also the fee payer / transaction source) can emit a
* contract signature. Used by Safe recovery, where the 2-of-3 contract requires owner
* signatures indexed per the on-chain storage keys.
*
* @param {KeyPair} keyPair - the owner key pair to sign with
* @param {number} index - the owner index in the contract storage keys list
*/
addContractSigner(keyPair: KeyPair, index: number): void {
if (index >= DEFAULT_M) {
throw new BuildTransactionError(
'Custom index cannot be greater than the wallet total number of signers (owners)'
);
}
this._multisigSignerKeyPairs.push({ key: keyPair, index });
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
// If the from() method was called, use the serialized transaction as a base
Expand Down
Loading
Loading