Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multisig key type #288

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 7 additions & 5 deletions packages/walletconnect/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
ApiRequestArguments,
NetworkId,
networkIds,
keyTypes,
EnableOptionsBase
} from '@alephium/web3'

Expand Down Expand Up @@ -547,12 +548,13 @@ export function formatAccount(permittedChain: string, account: Account): string
}

export function parseAccount(account: string): Account & { networkId: NetworkId } {
const [_namespace, networkId, _group, publicKey, keyType] = account.replace(/\//g, ':').split(':')
const address = addressFromPublicKey(publicKey)
const group = groupOfAddress(address)
if (keyType !== 'default' && keyType !== 'bip340-schnorr') {
throw Error(`Invalid key type: ${keyType}`)
const [_namespace, networkId, _group, publicKey, _keyType] = account.replace(/\//g, ':').split(':')
const keyType = keyTypes.find((tpe) => tpe === _keyType)
if (keyType === undefined) {
throw Error(`Invalid key type: ${_keyType}`)
}
const address = addressFromPublicKey(publicKey, keyType)
const group = groupOfAddress(address)
return { address, group, publicKey, keyType, networkId: networkId as NetworkId }
}

Expand Down
9 changes: 8 additions & 1 deletion packages/web3/src/api/node-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
requestWithLog
} from './types'
import { Api as NodeApi, CallContractFailed, CallContractSucceeded } from './api-alephium'
import { tryGetCallResult } from '../contract'
import {
HexString,
addressFromContractId,
Expand All @@ -38,6 +37,7 @@ import {
isHexString,
toNonNegativeBigInt
} from '../utils'
import * as node from '../api/api-alephium'

function initializeNodeApi(baseUrl: string, apiKey?: string, customFetch?: typeof fetch): NodeApi<string> {
const nodeApi = new NodeApi<string>({
Expand Down Expand Up @@ -253,3 +253,10 @@ export class NodeProvider implements NodeProviderApis {
}
}
}

export function tryGetCallResult(result: node.CallContractResult): node.CallContractSucceeded {
if (result.type === 'CallContractFailed') {
throw new Error(`Failed to call contract, error: ${(result as node.CallContractFailed).error}`)
}
return result as node.CallContractSucceeded
}
2 changes: 1 addition & 1 deletion packages/web3/src/api/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import 'cross-fetch/polyfill'
import { node } from '..'
import * as node from '../api/api-alephium'

export function convertHttpResponse<T>(response: { status: number; data: T; error?: { detail: string } }): T {
if (response.error) {
Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/codec/contract-output-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Codec } from './codec'
import { Token, tokensCodec } from './token-codec'
import { ContractOutput as ApiContractOutput } from '../api/api-alephium'
import { blakeHash, createHint } from './hash'
import { binToHex, bs58 } from '..'
import { binToHex, bs58 } from '../utils'
import { signedIntCodec } from './signed-int-codec'
import { lockupScriptCodec } from './lockup-script-codec'

Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/codec/lockup-script-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class PublicKeyHashCodec implements Codec<PublicKeyHash> {
}

const publicKeyHashCodec = new PublicKeyHashCodec()
const publicKeyHashesCodec = new ArrayCodec(publicKeyHashCodec)
export const publicKeyHashesCodec = new ArrayCodec(publicKeyHashCodec)
const multiSigParser = Parser.start()
.nest('publicKeyHashes', { type: publicKeyHashesCodec.parser })
.nest('m', { type: compactUnsignedIntCodec.parser })
Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/codec/transaction-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { Either } from './either-codec'
import { AssetOutput, AssetOutputCodec } from './asset-output-codec'
import { ContractOutput, ContractOutputCodec } from './contract-output-codec'
import { FixedAssetOutput, Transaction as ApiTransaction } from '../api/api-alephium'
import { hexToBinUnsafe } from '..'
import { hexToBinUnsafe } from '../utils'
import { ContractOutput as ApiContractOutput } from '../api/api-alephium'
import { Codec } from './codec'
import { Output, outputCodec, outputsCodec } from './output-codec'
Expand Down
10 changes: 2 additions & 8 deletions packages/web3/src/contract/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ import {
getDefaultPrimitiveValue,
PrimitiveTypes,
decodeArrayType,
fromApiPrimitiveVal
fromApiPrimitiveVal,
tryGetCallResult
} from '../api'
import { CompileProjectResult } from '../api/api-alephium'
import {
Expand Down Expand Up @@ -2369,10 +2370,3 @@ export const getContractIdFromUnsignedTx = async (

// This function only works in the simple case where a single non-subcontract is created in the tx
export const getTokenIdFromUnsignedTx = getContractIdFromUnsignedTx

export function tryGetCallResult(result: node.CallContractResult): node.CallContractSucceeded {
if (result.type === 'CallContractFailed') {
throw new Error(`Failed to call contract, error: ${(result as node.CallContractFailed).error}`)
}
return result as node.CallContractSucceeded
}
2 changes: 1 addition & 1 deletion packages/web3/src/contract/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { web3 } from '..'
import * as web3 from '../global'
import { node } from '../api'
import { Subscription, SubscribeOptions } from '../utils'

Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/contract/ralph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
import * as ralph from './ralph'
import * as utils from '../utils'
import { Fields, FieldsSig, Struct, fromApiArray, fromApiEventFields, fromApiFields, getDefaultValue } from './contract'
import { node } from '..'
import * as node from '../api/api-alephium'

describe('contract', function () {
it('should encode I256', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/web3/src/signer/tx-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { utils } from '..'
import { binToHex, contractIdFromAddress } from '../utils'
import { fromApiNumber256, node, NodeProvider, toApiNumber256Optional, toApiTokens } from '../api'
import { addressFromPublicKey } from '../utils'
import { toApiDestinations } from './signer'
Expand Down Expand Up @@ -90,7 +90,7 @@ export abstract class TransactionBuilder {
...rest
}
const response = await this.nodeProvider.contracts.postContractsUnsignedTxDeployContract(data)
const contractId = utils.binToHex(utils.contractIdFromAddress(response.contractAddress))
const contractId = binToHex(contractIdFromAddress(response.contractAddress))
return { ...response, groupIndex: response.fromGroup, contractId, gasPrice: fromApiNumber256(response.gasPrice) }
}

Expand Down
5 changes: 3 additions & 2 deletions packages/web3/src/signer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export interface Destination {
lockTime?: number
message?: string
}
assertType<Eq<keyof Destination, keyof node.Destination>>
assertType<Eq<keyof Destination, keyof node.Destination>>()

export type KeyType = 'default' | 'bip340-schnorr'
export const keyTypes = ['default', 'bip340-schnorr', 'multisig'] as const
export type KeyType = (typeof keyTypes)[number]

export interface Account {
keyType: KeyType
Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/transaction/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { web3 } from '..'
import * as web3 from '../global'
import { node } from '../api'
import { Subscription, SubscribeOptions } from '../utils'

Expand Down
78 changes: 77 additions & 1 deletion packages/web3/src/utils/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
tokenIdFromAddress,
validateAddress,
isAssetAddress,
isContractAddress
isContractAddress,
encodeMultisigPublicKeys
} from './address'
import { binToHex } from './utils'

Expand Down Expand Up @@ -118,13 +119,88 @@ describe('address', function () {
)
})

it('should encode multisig public keys', () => {
expect(() => encodeMultisigPublicKeys([], 2)).toThrow('Public key array is empty')
expect(() =>
encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 2)
).toThrow('Invalid m in m-of-n multisig, m: 2, n: 1')
expect(() =>
encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc', '0011'], 1)
).toThrow('Invalid public key: 0011')
expect(encodeMultisigPublicKeys(['030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'], 1)).toEqual(
'0101030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'
)
expect(
encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19'
],
1
)
).toEqual(
'0102030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19'
)
expect(
encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19',
'03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
],
2
)
).toEqual(
'0203030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
)
expect(
encodeMultisigPublicKeys(
[
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc',
'03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be19',
'03c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
],
3
)
).toEqual(
'0303030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc03c83325bd2c0fe1464161c6d5f42699fc9dd799dda7f984f9fbf59b01b095be1903c0a849d8ab8633b45b45ea7f3bb3229e1083a13fd73e027aac2bc55e7f622172'
)
})

it('should compute address from public key', () => {
expect(publicKeyFromPrivateKey('91411e484289ec7e8b3058697f53f9b26fa7305158b4ef1a81adfbabcf090e45')).toBe(
'030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc'
)
expect(addressFromPublicKey('030f9f042a9410969f1886f85fa20f6e43176ae23fc5e64db15b3767c84c5db2dc')).toBe(
'1ACCkgFfmTif46T3qK12znuWjb5Bk9jXpqaeWt2DXx8oc'
)
const publicKeys = [
'043ed1a15fa4b9c92d5f0b712c238cc26e16bfe8a359d6dc0aeffed983c02e800b',
'bcdfb4cbd7555f8df4b66414f17e81eaa108dea382dabb01a63ced575d1824b37e',
'94438313828b1b17e5d7f2c9d773d44a81af6c3ef67446fbf350497ff3b06c3741'
]
expect(() => addressFromPublicKey('0100', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: 0')
expect(() => addressFromPublicKey('013f', 'multisig')).toThrow('Invalid n in m-of-n multisig, m: 1, n: -1')
expect(() => addressFromPublicKey('0201', 'multisig')).toThrow('Invalid m in m-of-n multisig, m: 2, n: 1')
expect(() => addressFromPublicKey('3f02', 'multisig')).toThrow('Invalid m in m-of-n multisig, m: -1, n: 2')
expect(() => addressFromPublicKey('04' + encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')).toThrow(
'Invalid m in m-of-n multisig, m: 4, n: 3'
)
expect(() => addressFromPublicKey('00' + encodeMultisigPublicKeys(publicKeys, 3).slice(2), 'multisig')).toThrow(
'Invalid m in m-of-n multisig, m: 0, n: 3'
)
expect(() => addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 3).slice(0, -2), 'multisig')).toThrow(
'Invalid public key size'
)
expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 3), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMG'
)
expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 2), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEMF'
)
expect(addressFromPublicKey(encodeMultisigPublicKeys(publicKeys, 1), 'multisig')).toEqual(
'X15q3KSAid29imun4VPNCTHCNvdcB9Ji6LBp84t4TgUSLv5GvGAzAMT5PdhfWYAD1E8NcxHz5g5Ni9CE5ExRyXf8dXgg3WyEeCu9uWgohcvbtGa5QJ5Q5R33vnNPnxcvzeSEME'
)
})

it('should convert between contract id and address', () => {
Expand Down
63 changes: 61 additions & 2 deletions packages/web3/src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ import { TOTAL_NUMBER_OF_GROUPS } from '../constants'
import blake from 'blakejs'
import bs58 from './bs58'
import djb2 from './djb2'
import { binToHex, hexToBinUnsafe } from './utils'
import { binToHex, hexToBinUnsafe, isHexString } from './utils'
import { KeyType } from '../signer'
import { compactSignedIntCodec } from '../codec'
import * as codec from '../codec'
import { Buffer } from 'buffer/'

const ec = new EC('secp256k1')
const PublicKeyBytesLength = 33

export enum AddressType {
P2PKH = 0x00,
Expand Down Expand Up @@ -154,10 +158,65 @@ export function addressFromPublicKey(publicKey: string, _keyType?: KeyType): str
const hash = Buffer.from(blake.blake2b(Buffer.from(publicKey, 'hex'), undefined, 32))
const bytes = Buffer.concat([addressType, hash])
return bs58.encode(bytes)
} else {
} else if (keyType === 'bip340-schnorr') {
const lockupScript = Buffer.from(`0101000000000458144020${publicKey}8685`, 'hex')
return addressFromScript(lockupScript)
} else {
return multisigAddressFromPublicKey(publicKey)
}
}

function multisigAddressFromPublicKey(publicKey: string): string {
if (!isHexString(publicKey)) {
throw new Error(`Invalid public key ${publicKey}, expected a hex-string`)
}
const bytes = Buffer.from(hexToBinUnsafe(publicKey))
const decodedM = compactSignedIntCodec.decode(bytes)
let index = decodedM.rest.length + 1
const decodedN = compactSignedIntCodec.decode(bytes.slice(index))
index += decodedN.rest.length + 1
const m = compactSignedIntCodec.toI32(decodedM)
const n = compactSignedIntCodec.toI32(decodedN)
if (n <= 0) {
throw new Error(`Invalid n in m-of-n multisig, m: ${m}, n: ${n}`)
}
if (m <= 0 || m > n) {
throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${n}`)
}
if (bytes.length !== PublicKeyBytesLength * n + 2) {
throw new Error('Invalid public key size')
}

const publicKeyHashes: codec.lockupScript.PublicKeyHash[] = []
for (; index < bytes.length; index += 33) {
const publicKey = bytes.slice(index, index + 33)
publicKeyHashes.push({ publicKeyHash: Buffer.from(blake.blake2b(publicKey, undefined, 32)) })
}
const lockupScript: codec.lockupScript.LockupScript = {
scriptType: AddressType.P2MPKH,
script: {
publicKeyHashes: codec.lockupScript.publicKeyHashesCodec.fromArray(publicKeyHashes),
m: compactSignedIntCodec.fromI32(m)
}
}
const encoded = codec.lockupScript.lockupScriptCodec.encode(lockupScript)
return bs58.encode(encoded)
}

export function encodeMultisigPublicKeys(publicKeys: string[], m: number): string {
if (publicKeys.length === 0) {
throw new Error('Public key array is empty')
}
if (m <= 0 || m > publicKeys.length) {
throw new Error(`Invalid m in m-of-n multisig, m: ${m}, n: ${publicKeys.length}`)
}
publicKeys.forEach((publicKey) => {
if (!isHexString(publicKey) || publicKey.length !== PublicKeyBytesLength * 2) {
throw new Error(`Invalid public key: ${publicKey}`)
}
})
const prefix = Buffer.concat([compactSignedIntCodec.encodeI32(m), compactSignedIntCodec.encodeI32(publicKeys.length)])
return prefix.toString('hex') + publicKeys.join('')
}

export function addressFromScript(script: Uint8Array): string {
Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/utils/exchange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ You should have received a copy of the GNU Lesser General Public License
along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '..'
import { AddressType, addressFromPublicKey, addressFromScript, binToHex, bs58, hexToBinUnsafe } from '../utils'
import { Transaction } from '../api/api-alephium'
import { Address } from '../signer'

Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
// 2. https://github.com/ethers-io/ethers.js/blob/724881f34d428406488a1c9f9dbebe54b6edecda/src.ts/utils/fixednumber.ts

import BigNumber from 'bignumber.js'
import { Number256 } from '..'
import { Number256 } from '../api/types'

export const isNumeric = (numToCheck: any): boolean => !isNaN(parseFloat(numToCheck)) && isFinite(numToCheck)

Expand Down
2 changes: 1 addition & 1 deletion packages/web3/src/utils/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ along with the library. If not, see <http://www.gnu.org/licenses/>.
*/

import { ec as EC } from 'elliptic'
import { binToHex, encodeSignature, hexToBinUnsafe, signatureDecode } from '..'
import { binToHex, encodeSignature, hexToBinUnsafe, signatureDecode } from '../utils'
import { KeyType } from '../signer'
import * as necc from '@noble/secp256k1'
import { createHash, createHmac } from 'crypto'
Expand Down
Loading