UNPKG

@ceramicnetwork/blockchain-utils-linking

Version:

Blockchain utils for linking blockchain accounts to DID

181 lines • 7.18 kB
import { AccountId } from 'caip'; import { asOldCaipString, encodeRpcMessage, getConsentMessage, } from './util.js'; import * as uint8arrays from 'uint8arrays'; import * as sha256 from '@stablelib/sha256'; import { StreamID } from '@ceramicnetwork/streamid'; import { randomString } from '@stablelib/random'; import { Cacao, SiweMessage } from '@didtools/cacao'; const ADDRESS_TYPES = { ethereumEOA: 'ethereum-eoa', erc1271: 'erc1271', }; const CHAIN_NAMESPACE = 'eip155'; const chainIdCache = new WeakMap(); async function requestChainId(provider) { let chainId = chainIdCache.get(provider); if (!chainId) { const chainIdHex = await safeSend(provider, 'eth_chainId', []); chainId = parseInt(chainIdHex, 16); chainIdCache.set(provider, chainId); } return chainId; } export class EthereumAuthProvider { constructor(provider, address, opts = {}) { this.provider = provider; this.address = address; this.opts = opts; this.isAuthProvider = true; } async accountId() { if (!this._accountId) { const chainId = await requestChainId(this.provider); this._accountId = new AccountId({ address: this.address, chainId: `${CHAIN_NAMESPACE}:${chainId}`, }); } return this._accountId; } async authenticate(message) { const accountId = await this.accountId(); return authenticate(message, accountId, this.provider); } async createLink(did) { const accountId = await this.accountId(); return createLink(did, accountId, this.provider, this.opts); } async requestCapability(sessionDID, streams, opts = {}) { console.warn('WARN: requestCapability os an experimental API, that is subject to change any time.'); const domain = typeof window !== 'undefined' ? window.location.hostname : opts.domain; if (!domain) throw new Error("Missing parameter 'domain'"); const now = new Date(); const oneDayLater = new Date(now.getTime() + 24 * 60 * 60 * 1000); const siweMessage = new SiweMessage({ domain: domain, address: this.address, statement: opts.statement ?? 'Give this application access to some of your data on Ceramic', uri: sessionDID, version: opts.version ?? '1', nonce: opts.nonce ?? randomString(10), issuedAt: now.toISOString(), expirationTime: opts.expirationTime ?? oneDayLater.toISOString(), chainId: (await this.accountId()).chainId.reference, resources: (opts.resources ?? []).concat(streams.map((s) => (typeof s === 'string' ? StreamID.fromString(s) : s).toUrl())), }); if (opts.requestId) siweMessage.requestId = opts.requestId; const account = await this.accountId(); const signature = await safeSend(this.provider, 'personal_sign', [ siweMessage.signMessage(), account.address, ]); siweMessage.signature = signature; const cacao = Cacao.fromSiweMessage(siweMessage); return cacao; } withAddress(address) { return new EthereumAuthProvider(this.provider, address); } } export function isEthAddress(address) { return /^0x[a-fA-F0-9]{40}$/.test(address); } async function getCode(address, provider) { return safeSend(provider, 'eth_getCode', [address, 'latest']); } function safeSend(provider, method, params) { if (params == null) { params = []; } if (provider.request) { return provider.request({ method, params }); } else if (provider.sendAsync || provider.send) { const sendFunc = (provider.sendAsync ? provider.sendAsync : provider.send).bind(provider); const request = encodeRpcMessage(method, params); return new Promise((resolve, reject) => { sendFunc(request, (error, response) => { if (error) reject(error); if (response.error) { const error = new Error(response.error.message); error.code = response.error.code; error.data = response.error.data; reject(error); } resolve(response.result); }); }); } else { throw new Error(`Unsupported provider; provider must implement one of the following methods: send, sendAsync, request`); } } export async function isERC1271(account, provider) { const bytecode = await getCode(account.address, provider).catch(() => null); return Boolean(bytecode && bytecode !== '0x' && bytecode !== '0x0' && bytecode !== '0x00'); } export function normalizeAccountId(input) { return new AccountId({ address: input.address.toLowerCase(), chainId: input.chainId, }); } function utf8toHex(message) { const bytes = uint8arrays.fromString(message); const hex = uint8arrays.toString(bytes, 'base16'); return '0x' + hex; } async function createEthLink(did, account, provider, opts = {}) { const { message, timestamp } = getConsentMessage(did, !opts.skipTimestamp); const hexMessage = utf8toHex(message); const signature = await safeSend(provider, 'personal_sign', [hexMessage, account.address]); const proof = { version: 2, type: ADDRESS_TYPES.ethereumEOA, message, signature, account: asOldCaipString(account), }; if (!opts.skipTimestamp) proof.timestamp = timestamp; return proof; } async function validateChainId(account, provider) { const chainId = await requestChainId(provider); if (chainId !== parseInt(account.chainId.reference)) { throw new Error(`ChainId in provider (${chainId}) is different from AccountId (${account.chainId.reference})`); } } async function createErc1271Link(did, account, provider, opts) { const ethLinkAccount = opts?.eoaSignAccount || account; const res = await createEthLink(did, ethLinkAccount, provider, opts); await validateChainId(account, provider); return Object.assign(res, { type: ADDRESS_TYPES.erc1271, account: asOldCaipString(account), }); } export async function createLink(did, account, provider, opts) { account = normalizeAccountId(account); if (await isERC1271(account, provider)) { return createErc1271Link(did, account, provider, opts); } else { return createEthLink(did, account, provider, opts); } } export async function authenticate(message, account, provider) { if (account) account = normalizeAccountId(account); if (provider.isAuthereum) return provider.signMessageWithSigningKey(message); const hexMessage = utf8toHex(message); const signature = await safeSend(provider, 'personal_sign', [hexMessage, account.address]); const signatureBytes = uint8arrays.fromString(signature.slice(2)); const digest = sha256.hash(signatureBytes); return `0x${uint8arrays.toString(digest, 'base16')}`; } //# sourceMappingURL=ethereum.js.map