@ceramicnetwork/blockchain-utils-linking
Version:
Blockchain utils for linking blockchain accounts to DID
181 lines • 7.18 kB
JavaScript
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