UNPKG

iso-filecoin

Version:

Isomorphic filecoin abstractions for RPC, signatures, address, token and wallet

988 lines (862 loc) 26 kB
'use strict'; var blake2b = require('../node_modules/.pnpm/@noble_hashes@1.8.0/node_modules/@noble/hashes/esm/blake2b.cjs'); var leb128 = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/leb128.cjs'); var rfc4648 = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/rfc4648.cjs'); var utils = require('../node_modules/.pnpm/iso-base@4.1.0/node_modules/iso-base/src/utils.cjs'); var src_utils = require('./utils.cjs'); /** * Filecoin address * * @module */ /** * @import {AddressRpcOptions, AddressRpcSafetyOptions} from './types.js' */ /** * @typedef {import('./types.js').IAddress} IAddress * @typedef { string | IAddress | BufferSource} Value */ /** * Protocol indicator */ const PROTOCOL_INDICATOR = /** @type {const} */ ({ ID: 0, SECP256K1: 1, ACTOR: 2, BLS: 3, DELEGATED: 4, }); const symbol = Symbol.for('filecoin-address'); /** * Asserts that the given value is an {@link IAddress} instance. * * @example * ```ts twoslash * import { isAddress, fromString } from 'iso-filecoin/address' * * const address = isAddress(fromString('f1...')) // true * const notAddress = isAddress('f1...') // falseeeeeee * ``` * * @param {any} val * @returns {val is IAddress} */ function isAddress(val) { return Boolean(val?.[symbol]) } /** * Check if object is a {@link AddressSecp256k1} instance * * @param {any} val * @returns {val is AddressSecp256k1} */ function isAddressSecp256k1(val) { return Boolean(val?.[symbol]) && val.protocol === PROTOCOL_INDICATOR.SECP256K1 } /** * Check if object is a {@link AddressBLS} instance * * @param {any} val * @returns {val is AddressBLS} */ function isAddressBls(val) { return Boolean(val?.[symbol]) && val.protocol === PROTOCOL_INDICATOR.BLS } /** * Check if object is a {@link AddressId} instance * * @param {any} val * @returns {val is AddressId} */ function isAddressId(val) { return Boolean(val?.[symbol]) && val.protocol === PROTOCOL_INDICATOR.ID } /** * Check if object is a {@link AddressDelegated} instance * * @param {any} val * @returns {val is AddressDelegated} */ function isAddressDelegated(val) { return Boolean(val?.[symbol]) && val.protocol === PROTOCOL_INDICATOR.DELEGATED } /** * Validate checksum * * @param {Uint8Array} actual * @param {Uint8Array} expected */ function validateChecksum(actual, expected) { return utils.equals(actual, expected) } /** * Check if string is valid Ethereum address * * Based on viem implementation {@link https://github.com/wevm/viem/blob/main/src/utils/address/isAddress.ts} * * @param {string} address */ function isEthAddress(address) { if (!/^0x[a-fA-F0-9]{40}$/.test(address)) return false if (address.toLowerCase() === address) return true return src_utils.checksumEthAddress(address) === address } /** * Checks if address is an Ethereum ID mask address * * @param {string} address */ function isIdMaskAddress(address) { if (!isEthAddress(address)) { return false } const bytes = rfc4648.hex.decode(address.substring(2)); const idMaskPrefix = new Uint8Array(12).fill(255, 0, 1); return utils.equals(bytes.slice(0, 12), idMaskPrefix) } /** * Address from Ethereum address * * @param {string} address * @param {import('./types.js').Network} network * @returns {IAddress} */ function fromEthAddress(address, network) { if (isIdMaskAddress(address)) { return AddressId.fromIdMaskAddress(address, network) } return AddressDelegated.fromEthAddress(address, network) } /** * Ethereum address from f0 or f4 addresses * * @param {IAddress} address */ function toEthAddress(address) { if (address.protocol === PROTOCOL_INDICATOR.ID) { return /** @type {AddressId} */ (address).toIdMaskAddress() } if (address.protocol === PROTOCOL_INDICATOR.DELEGATED) { return /** @type {AddressDelegated} */ (address).toEthAddress() } throw new Error( `Invalid protocol indicator: ${address.protocol}. Only Delegated ad ID Addresses are supported.` ) } /** * @param {Value} value - Value to convert to address * @param {import('./types.js').Network} [network] - Network * @returns {IAddress} */ function from(value, network = 'mainnet') { if (utils.isBufferSource(value)) { return fromBytes(utils.u8(value), network) } if (isAddress(value)) { return value } if (typeof value === 'string') { return isEthAddress(value) ? fromEthAddress(value, network) : fromString(value) } throw new Error(`Invalid value: ${value}`) } /** * Address from string * * @param {string} address * @returns {IAddress} */ function fromString(address) { const type = Number.parseInt(address[1]); switch (type) { case PROTOCOL_INDICATOR.SECP256K1: { return AddressSecp256k1.fromString(address) } case PROTOCOL_INDICATOR.DELEGATED: { return AddressDelegated.fromString(address) } case PROTOCOL_INDICATOR.ACTOR: { return AddressActor.fromString(address) } case PROTOCOL_INDICATOR.BLS: { return AddressBLS.fromString(address) } case PROTOCOL_INDICATOR.ID: { return AddressId.fromString(address) } default: { throw new Error(`Invalid protocol indicator: ${type}`) } } } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @returns {IAddress} */ function fromBytes(bytes, network) { const type = bytes[0]; switch (type) { case PROTOCOL_INDICATOR.SECP256K1: { return AddressSecp256k1.fromBytes(bytes, network) } case PROTOCOL_INDICATOR.DELEGATED: { return AddressDelegated.fromBytes(bytes, network) } case PROTOCOL_INDICATOR.BLS: { return AddressBLS.fromBytes(bytes, network) } case PROTOCOL_INDICATOR.ACTOR: { return AddressActor.fromBytes(bytes, network) } case PROTOCOL_INDICATOR.ID: { return AddressId.fromBytes(bytes, network) } default: { throw new Error(`Invalid protocol indicator: ${type}`) } } } /** * Create address from public key bytes * Only for f1 SECP256K1 and f3 BLS * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @param {import('./types.js').SignatureType} type * @returns IAddress */ function fromPublicKey(bytes, network, type) { switch (type) { case 'SECP256K1': { return AddressSecp256k1.fromPublicKey(bytes, network) } case 'BLS': { return AddressBLS.fromPublicKey(bytes, network) } default: { throw new Error(`Invalid signature type: ${type}`) } } } /** * Create an `Address` instance from a 0x-prefixed hex string address returned by `Address.toContractDestination()`. * * @param {`0x${string}`} address - The 0x-prefixed hex string address. * @param {import("./types.js").Network} network - The network the address is on. */ function fromContractDestination(address, network) { if (!address.startsWith('0x')) { throw new Error(`Expected 0x prefixed hex, instead got: '${address}'`) } return fromBytes(rfc4648.hex.decode(address.slice(2).toLowerCase()), network) } /** * Generic address class * * @implements {IAddress} */ class Address { /** @type {boolean} */ [symbol] = true /** * * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(payload, network) { this.payload = payload; this.network = network; this.networkPrefix = src_utils.NETWORKS[network]; /** @type {import('./types.js').ProtocolIndicatorCode} */ this.protocol = PROTOCOL_INDICATOR.ID; } toString() { return `${this.networkPrefix}${this.protocol}${rfc4648.base32 .encode(utils.concat([this.payload, this.checksum()]), false) .toLowerCase()}` } toBytes() { return utils.concat([rfc4648.hex.decode(`0${this.protocol}`), this.payload]) } toContractDestination() { return /** @type {`0x${string}`} */ (`0x${rfc4648.hex.encode(this.toBytes())}`) } checksum() { return blake2b.blake2b(this.toBytes(), { dkLen: 4, }) } /** * @inheritdoc IAddress.toIdAddress * @param {AddressRpcSafetyOptions} options * @returns {Promise<AddressId>} */ async toIdAddress(options) { const { rpc } = options; if (rpc.network !== this.network) { throw new Error( `Network mismatch. RPC network: ${rpc.network} Address network: ${this.network}` ) } const cache = src_utils.getCache(options.cache); const key = ['id', this.toString()]; let idAddress; if (isAddressId(this)) { idAddress = /** @type {AddressId} */ (this); } const cached = await /** @type {typeof cache.get<string>}*/ (cache.get)(key); if (cached && options.safety === 'finalized') { return AddressId.fromString(cached) } if (isAddressDelegated(this)) { const id = await rpc.getIDAddress({ address: this.toString(), safety: options.safety ?? 'finalized', }); if (id.error) { throw new Error(id.error.message) } idAddress = AddressId.fromString(id.result); } else { // f1,f2 and f3 uses the faster endpoint idAddress = AddressId.fromIdMaskAddress( await this.to0x(options), this.network ); } if (options.safety === 'finalized') { await cache.set(key, idAddress.toString()); } return idAddress } /** * @inheritdoc IAddress.to0x * @param {AddressRpcSafetyOptions} options * @returns {Promise<string>} */ async to0x(options) { const { rpc } = options; if (rpc.network !== this.network) { throw new Error( `Network mismatch. RPC network: ${rpc.network} Address network: ${this.network}` ) } const cache = src_utils.getCache(options.cache); const key = ['0x', this.toString()]; const cached = await /** @type {typeof cache.get<string>}*/ (cache.get)(key); if (cached && options.safety === 'finalized') { return cached } // f1,f2 and f3 uses the faster endpoint const r = await rpc.filecoinAddressToEthAddress({ address: this.toString(), blockNumber: options.safety ?? 'finalized', }); if (r.error) { throw new Error(r.error.message) } if (!isIdMaskAddress(r.result)) { throw new Error(`Invalid ID masked 0x address: ${r.result}`) } if (options.safety === 'finalized') { await cache.set(key, r.result); } return r.result } } /** * ID Address f0.. * * Protocol 0 addresses are simple IDs. All actors have a numeric ID even if they don’t have public keys. The payload of an ID address is base10 encoded. IDs are not hashed and do not have a checksum. * * @see https://spec.filecoin.io/appendix/address/#section-appendix.address.protocol-0-ids * * @implements {IAddress} */ class AddressId extends Address { /** * * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(payload, network) { super(payload, network); this.protocol = PROTOCOL_INDICATOR.ID; this.id = leb128.unsigned.decode(payload)[0]; } /** * Create address from string * * @param {string} address */ static fromString(address) { const networkPrefix = address[0]; const protocolIndicator = address[1]; if (!src_utils.checkNetworkPrefix(networkPrefix)) { throw new Error(`Invalid network: ${networkPrefix}`) } if (Number.parseInt(protocolIndicator) !== PROTOCOL_INDICATOR.ID) { throw new Error(`Invalid protocol indicator: ${protocolIndicator}`) } const newAddress = new AddressId( leb128.unsigned.encode(address.slice(2)), src_utils.getNetwork(networkPrefix) ); return newAddress } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network */ static fromBytes(bytes, network) { if (bytes[0] !== PROTOCOL_INDICATOR.ID) { throw new Error(`Invalid protocol indicator: ${bytes[0]}`) } return new AddressId(bytes.subarray(1), network) } /** * Create ID address from ID masked 0x address * * @param {string} address * @param {import('./types.js').Network} network */ static fromIdMaskAddress(address, network) { if (!isIdMaskAddress(address)) { throw new Error(`Invalid Ethereum ID mask address: ${address}`) } const bytes = rfc4648.hex.decode(address.slice(2)); if (bytes.length !== 20) { throw new Error( `Invalid Ethereum payload length: ${bytes.length} should be 20.` ) } const dataview = new DataView(bytes.buffer); const idBigInt = dataview.getBigUint64(12, false); const leb128Id = leb128.unsigned.encode(idBigInt); return new AddressId(leb128Id, network) } /** * Convert address to ID masked 0x address * * To convert to an eth address you probably should use {@link to0x} */ toIdMaskAddress() { const buf = new ArrayBuffer(20); const dataview = new DataView(buf); dataview.setUint8(0, 255); dataview.setBigUint64(12, this.id, false); return src_utils.checksumEthAddress(`0x${rfc4648.hex.encode(new Uint8Array(buf))}`) } toString() { return `${this.networkPrefix}${this.protocol}${this.id}` } /** * Get robust address from public key address * * @param {AddressRpcOptions} options */ async toRobust(options) { const { rpc } = options; if (rpc.network !== this.network) { throw new Error( `Network mismatch. RPC network: ${rpc.network} Address network: ${this.network}` ) } const cache = src_utils.getCache(options.cache); const key = ['robust', this.toString()]; const cached = await /** @type {typeof cache.get<string>}*/ (cache.get)(key); if (cached) { return fromString(cached) } const r = await rpc.stateAccountKey({ address: this.toString() }); if (r.error) { throw new Error(r.error.message) } const robust = fromString(r.result); await cache.set(key, robust.toString()); return robust } /** * @param {AddressRpcOptions} options */ async to0x(options) { const { rpc } = options; if (rpc.network !== this.network) { throw new Error( `Network mismatch. RPC network: ${rpc.network} Address network: ${this.network}` ) } const cache = src_utils.getCache(options.cache); const key = ['0x', this.toString()]; const cached = await /** @type {typeof cache.get<string>}*/ (cache.get)(key); if (cached) { return cached } const robust = await this.toRobust(options); const eth = await robust.to0x(options); await cache.set(key, eth); return eth } } /** * Secp256k1 address f1.. * * @see https://spec.filecoin.io/appendix/address/#section-appendix.address.protocol-1-libsecpk1-elliptic-curve-public-keys * * @implements {IAddress} */ class AddressSecp256k1 extends Address { /** * * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(payload, network) { super(payload, network); this.protocol = PROTOCOL_INDICATOR.SECP256K1; if (payload.length !== 20) { throw new Error(`Invalid payload length: ${payload.length} should be 20.`) } } /** * Create address from string * * @param {string} address */ static fromString(address) { const networkPrefix = address[0]; const protocolIndicator = address[1]; if (!src_utils.checkNetworkPrefix(networkPrefix)) { throw new Error(`Invalid network: ${networkPrefix}`) } if (Number.parseInt(protocolIndicator) !== PROTOCOL_INDICATOR.SECP256K1) { throw new Error(`Invalid protocol indicator: ${protocolIndicator}`) } const data = rfc4648.base32.decode(address.slice(2).toUpperCase()); const payload = data.subarray(0, -4); const checksum = data.subarray(-4); const newAddress = new AddressSecp256k1(payload, src_utils.getNetwork(networkPrefix)); if (validateChecksum(newAddress.checksum(), checksum)) { return newAddress } throw new Error('Invalid checksum') } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @returns */ static fromBytes(bytes, network) { if (bytes[0] !== PROTOCOL_INDICATOR.SECP256K1) { throw new Error(`Invalid protocol indicator: ${bytes[0]}`) } return new AddressSecp256k1(bytes.subarray(1), network) } /** * @param {Uint8Array} publicKey * @param {import('./types.js').Network} network */ static fromPublicKey(publicKey, network) { if (publicKey.length !== 65) { throw new Error( `Invalid public key length: ${publicKey.length} should be 65.` ) } const payload = blake2b.blake2b(publicKey, { dkLen: 20, }); return new AddressSecp256k1(payload, network) } } /** * Actor Address f2.. * * Protocol 2 addresses representing an Actor. The payload field contains the SHA256 hash of meaningful data produced as a result of creating the actor. * * @see https://spec.filecoin.io/appendix/address/#section-appendix.address.protocol-2-actor * * @implements {IAddress} */ class AddressActor extends Address { /** * * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(payload, network) { super(payload, network); this.protocol = PROTOCOL_INDICATOR.ACTOR; if (payload.length !== 20) { throw new Error(`Invalid payload length: ${payload.length} should be 20.`) } } /** * Create address from string * * @param {string} address */ static fromString(address) { const networkPrefix = address[0]; const protocolIndicator = address[1]; if (!src_utils.checkNetworkPrefix(networkPrefix)) { throw new Error(`Invalid network: ${networkPrefix}`) } if (Number.parseInt(protocolIndicator) !== PROTOCOL_INDICATOR.ACTOR) { throw new Error(`Invalid protocol indicator: ${protocolIndicator}`) } const data = rfc4648.base32.decode(address.slice(2).toUpperCase()); const payload = data.subarray(0, -4); const checksum = data.subarray(-4); const newAddress = new AddressActor(payload, src_utils.getNetwork(networkPrefix)); if (validateChecksum(newAddress.checksum(), checksum)) { return newAddress } throw new Error('Invalid checksum') } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @returns */ static fromBytes(bytes, network) { if (bytes[0] !== PROTOCOL_INDICATOR.ACTOR) { throw new Error(`Invalid protocol indicator: ${bytes[0]}`) } return new AddressActor(bytes.subarray(1), network) } } /** * BLS Address f3.. * * Protocol 3 addresses represent BLS public encryption keys. The payload field contains the BLS public key. * * @see https://spec.filecoin.io/appendix/address/#section-appendix.address.protocol-3-bls * * @implements {IAddress} */ class AddressBLS extends Address { /** * * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(payload, network) { super(payload, network); this.protocol = PROTOCOL_INDICATOR.BLS; if (payload.length !== 48) { throw new Error(`Invalid payload length: ${payload.length} should be 48.`) } } /** * Create address from string * * @param {string} address */ static fromString(address) { const networkPrefix = address[0]; const protocolIndicator = address[1]; if (!src_utils.checkNetworkPrefix(networkPrefix)) { throw new Error(`Invalid network: ${networkPrefix}`) } if (Number.parseInt(protocolIndicator) !== PROTOCOL_INDICATOR.BLS) { throw new Error( `Invalid protocol indicator: ${protocolIndicator} expected ${PROTOCOL_INDICATOR.BLS}` ) } const data = rfc4648.base32.decode(address.slice(2).toUpperCase()); const payload = data.subarray(0, -4); const checksum = data.subarray(-4); const newAddress = new AddressBLS(payload, src_utils.getNetwork(networkPrefix)); if (validateChecksum(newAddress.checksum(), checksum)) { return newAddress } throw new Error('Invalid checksum') } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @returns */ static fromBytes(bytes, network) { if (bytes[0] !== PROTOCOL_INDICATOR.BLS) { throw new Error(`Invalid protocol indicator: ${bytes[0]}`) } return new AddressBLS(bytes.subarray(1), network) } /** * * @param {Uint8Array} publicKey * @param {import('./types.js').Network} network */ static fromPublicKey(publicKey, network) { if (publicKey.length !== 48) { throw new Error( `Invalid public key length: ${publicKey.length} should be 48.` ) } return new AddressBLS(publicKey, network) } } /** * Delegated address f4.. * * @see https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0048.md * * @implements {IAddress} */ class AddressDelegated extends Address { /** * @param {number} namespace * @param {Uint8Array} payload * @param {import("./types.js").Network} network */ constructor(namespace, payload, network) { super(payload, network); this.protocol = PROTOCOL_INDICATOR.DELEGATED; this.namespace = namespace; if (namespace !== 10) { throw new Error( `Invalid namespace: ${namespace}. Only Ethereum Address Manager (EAM) is supported.` ) } if (payload.length === 0 || payload.length > 54) { throw new Error( `Invalid payload length: ${payload.length} should be 54 bytes or less.` ) } } /** * Create address from string * * @param {string} address */ static fromString(address) { const networkPrefix = address[0]; const protocolIndicator = address[1]; if (!src_utils.checkNetworkPrefix(networkPrefix)) { throw new Error(`Invalid network: ${networkPrefix}`) } if (Number.parseInt(protocolIndicator) !== PROTOCOL_INDICATOR.DELEGATED) { throw new Error(`Invalid protocol indicator: ${protocolIndicator}`) } const namespace = address.slice(2, address.indexOf('f', 2)); const dataEncoded = address.slice(address.indexOf('f', 2) + 1); const data = rfc4648.base32.decode(dataEncoded.toUpperCase()); const payload = data.subarray(0, -4); const checksum = data.subarray(-4); const newAddress = new AddressDelegated( Number.parseInt(namespace), payload, src_utils.getNetwork(networkPrefix) ); if (validateChecksum(newAddress.checksum(), checksum)) { return newAddress } throw new Error('Invalid checksum') } /** * Create address from bytes * * @param {Uint8Array} bytes * @param {import('./types.js').Network} network * @returns */ static fromBytes(bytes, network) { if (bytes[0] !== PROTOCOL_INDICATOR.DELEGATED) { throw new Error(`Invalid protocol indicator: ${bytes[0]}`) } const [namespace, size] = leb128.unsigned.decode(bytes, 1); return new AddressDelegated( Number(namespace), bytes.subarray(1 + size), network ) } /** * Create delegated address from ethereum address * * @param {string} address * @param {import('./types.js').Network} network */ static fromEthAddress(address, network) { if (!isEthAddress(address)) { throw new Error(`Invalid Ethereum address: ${address}`) } if (isIdMaskAddress(address)) { throw new Error(`Cannot convert Ethereum ID mask address: ${address}`) } const bytes = rfc4648.hex.decode(address.slice(2).toLowerCase()); if (bytes.length !== 20) { throw new Error( `Invalid Ethereum payload length: ${bytes.length} should be 20.` ) } return new AddressDelegated(10, bytes, network) } /** * Convert address to ethereum address * * @param {AddressRpcOptions} _rpc * @returns {Promise<string>} */ to0x(_rpc) { return Promise.resolve(this.toEthAddress()) } /** * Converts to 0x eth address, it's similar to {@link to0x} but sync * because f4s dont need to check the chain to get the address * */ toEthAddress() { if (this.payload.length > 20) { throw new Error( `Invalid payload length: ${this.payload.length} should be 20.` ) } return src_utils.checksumEthAddress(`0x${rfc4648.hex.encode(this.payload)}`) } toString() { return `${this.networkPrefix}${this.protocol}${this.namespace}f${rfc4648.base32 .encode(utils.concat([this.payload, this.checksum()]), false) .toLowerCase()}` } toBytes() { const protocol = leb128.unsigned.encode(this.protocol); const namespace = leb128.unsigned.encode(this.namespace); return utils.concat([protocol, namespace, this.payload]) } } exports.checksumEthAddress = src_utils.checksumEthAddress; exports.AddressActor = AddressActor; exports.AddressBLS = AddressBLS; exports.AddressDelegated = AddressDelegated; exports.AddressId = AddressId; exports.AddressSecp256k1 = AddressSecp256k1; exports.PROTOCOL_INDICATOR = PROTOCOL_INDICATOR; exports.from = from; exports.fromBytes = fromBytes; exports.fromContractDestination = fromContractDestination; exports.fromEthAddress = fromEthAddress; exports.fromPublicKey = fromPublicKey; exports.fromString = fromString; exports.isAddress = isAddress; exports.isAddressBls = isAddressBls; exports.isAddressDelegated = isAddressDelegated; exports.isAddressId = isAddressId; exports.isAddressSecp256k1 = isAddressSecp256k1; exports.isEthAddress = isEthAddress; exports.isIdMaskAddress = isIdMaskAddress; exports.toEthAddress = toEthAddress;