a multi-provider and tool for interacting with deployed contracts (#763)

* wip: a multi-provider and tool for seeing deployed contracts

* refactor: break things out a bit

* feature: loadJson

* feature: OpticsContext extends MultiProvider

* feature: hardcode mainnet deploy

* bugs: empty ethHelper and domain handling

* feature: resolveDomain

* feature: resolve token info

* feature: draft send implementation

* feature: imporve sendCoins.ts

* feature: OpticsMessage class

* feature: BridgeMessage extends OpticsMessage

* refactor: move all optics-specific behavior into folder

* refactor: rearrange contracts and imporve exports

* feature: parseMessage

* refactor: simplify and improve message classes

* feature: re-implement status call on OpticsMessage

* feature: resolve asset on BridgeMessage

* refactor: BridgeMessage is generic over action type

* bug: properly export new BridgeMessage variants

* feature: add a metamask file

* feature: dev and staging + bug: improper signer registration

* refactor: abstract out reconnection
buddies-main-deployment
James Prestwich 3 years ago committed by GitHub
parent 73e80a063c
commit e630652fc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      typescript/optics-provider/.prettierrc
  2. 1319
      typescript/optics-provider/package-lock.json
  3. 14
      typescript/optics-provider/package.json
  4. 26
      typescript/optics-provider/src/contracts.ts
  5. 4
      typescript/optics-provider/src/domains.ts
  6. 15
      typescript/optics-provider/src/index.ts
  7. 45
      typescript/optics-provider/src/metamask.ts
  8. 234
      typescript/optics-provider/src/optics/OpticsContext.ts
  9. 67
      typescript/optics-provider/src/optics/contracts/BridgeContracts.ts
  10. 81
      typescript/optics-provider/src/optics/contracts/CoreContracts.ts
  11. 47
      typescript/optics-provider/src/optics/domains/dev.ts
  12. 14
      typescript/optics-provider/src/optics/domains/domain.ts
  13. 4
      typescript/optics-provider/src/optics/domains/index.ts
  14. 50
      typescript/optics-provider/src/optics/domains/mainnet.ts
  15. 47
      typescript/optics-provider/src/optics/domains/staging.ts
  16. 17
      typescript/optics-provider/src/optics/index.ts
  17. 166
      typescript/optics-provider/src/optics/messages/BridgeMessage.ts
  18. 106
      typescript/optics-provider/src/optics/messages/OpticsMessage.ts
  19. 34
      typescript/optics-provider/src/optics/sendCoins.ts
  20. 13
      typescript/optics-provider/src/optics/tokens.ts
  21. 110
      typescript/optics-provider/src/provider.ts
  22. 16
      typescript/optics-provider/src/utils.ts
  23. 72
      typescript/optics-provider/tsconfig.json

@ -0,0 +1,5 @@
{
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,14 @@
{
"name": "optics-provider",
"version": "0.0.0",
"description": "multi-provider for Optics",
"main": "src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Celo Labs Inc.",
"license": "MIT OR Apache-2.0",
"dependencies": {
"ethers": "^5.4.6"
}
}

@ -0,0 +1,26 @@
import { ethers } from 'ethers';
import fs from 'fs';
export abstract class Contracts {
readonly args: any;
constructor(...args: any) {
this.args = args;
}
abstract toObject(): any;
abstract connect(signer: ethers.Signer): void;
toJson(): string {
return JSON.stringify(this.toObject());
}
toJsonPretty(): string {
return JSON.stringify(this.toObject(), null, 2);
}
saveJson(filepath: string) {
fs.writeFileSync(filepath, this.toJsonPretty());
}
}

@ -0,0 +1,4 @@
export interface Domain {
domain: number;
name: string;
}

@ -0,0 +1,15 @@
export { MultiProvider } from './provider';
export { mainnet, dev, staging, OpticsContext } from './optics';
// intended usage
// import {mainnet} from 'optics-provider';
// mainnet.registerRpcProvider('celo', 'https://forno.celo.org');
// mainnet.registerRpcProvider('polygon', '...');
// mainnet.registerRpcProvider('ethereum', '...');
// mainnet.registerSigner('celo', ...);
// mainnet.registerSigner('polygon', ...);
// mainnet.registerSigner('ethereum', ...);
// mainnet.doWhatever

@ -0,0 +1,45 @@
export type MetamaskNetwork = {
chainId: string;
chainName: string;
nativeCurrency: { name: string; symbol: string; decimals: number };
rpcUrls: string[];
blockExplorerUrls: string[];
iconUrls: string[];
};
export const CELO_PARAMS: MetamaskNetwork = {
chainId: '0xa4ec',
chainName: 'Celo',
nativeCurrency: { name: 'Celo', symbol: 'CELO', decimals: 18 },
rpcUrls: ['https://forno.celo.org'],
blockExplorerUrls: ['https://explorer.celo.org/'],
iconUrls: ['future'],
};
export const ALFAJORES_PARAMS: MetamaskNetwork = {
chainId: '0xaef3',
chainName: 'Alfajores Testnet',
nativeCurrency: { name: 'Alfajores Celo', symbol: 'A-CELO', decimals: 18 },
rpcUrls: ['https://alfajores-forno.celo-testnet.org'],
blockExplorerUrls: ['https://alfajores-blockscout.celo-testnet.org/'],
iconUrls: ['future'],
};
export const BAKLAVA_PARAMS: MetamaskNetwork = {
chainId: '0xf370',
chainName: 'Baklava Testnet',
nativeCurrency: { name: 'Baklava Celo', symbol: 'B-CELO', decimals: 18 },
rpcUrls: ['https://baklava-forno.celo-testnet.org'],
blockExplorerUrls: ['https://baklava-blockscout.celo-testnet.org/'],
iconUrls: ['future'],
};
export async function connect(params: MetamaskNetwork) {
const w = window as any;
if (w.ethereum) {
await w.ethereum.request({
method: 'wallet_addEthereumChain',
params: [params],
});
}
}

@ -0,0 +1,234 @@
import { BigNumberish, ContractTransaction, ethers } from 'ethers';
import { MultiProvider } from '..';
import { ERC20, ERC20__factory } from '../../../typechain/optics-xapps';
import { BridgeContracts } from './contracts/BridgeContracts';
import { CoreContracts } from './contracts/CoreContracts';
import { ResolvedTokenInfo, TokenIdentifier } from './tokens';
import { canonizeId } from '../utils';
import {
devDomains,
mainnetDomains,
OpticsDomain,
stagingDomains,
} from './domains';
import { Replica } from '../../../typechain/optics-core';
type Address = string;
export class OpticsContext extends MultiProvider {
private cores: Map<number, CoreContracts>;
private bridges: Map<number, BridgeContracts>;
constructor(
domains: OpticsDomain[],
cores: CoreContracts[],
bridges: BridgeContracts[],
) {
super();
domains.forEach((domain) => this.registerDomain(domain));
this.cores = new Map();
this.bridges = new Map();
cores.forEach((core) => {
this.cores.set(core.domain, core);
});
bridges.forEach((bridge) => {
this.bridges.set(bridge.domain, bridge);
});
}
static fromDomains(domains: OpticsDomain[]): OpticsContext {
const cores = domains.map((domain) => CoreContracts.fromObject(domain));
const bridges = domains.map((domain) => BridgeContracts.fromObject(domain));
return new OpticsContext(domains, cores, bridges);
}
private reconnect(domain: number) {
const connection = this.getConnection(domain);
if (!connection) {
throw new Error('Reconnect failed: no connection');
}
// re-register contracts
const core = this.cores.get(domain);
if (core) {
core.connect(connection);
}
const bridge = this.bridges.get(domain);
if (bridge) {
bridge.connect(connection);
}
}
registerProvider(
nameOrDomain: string | number,
provider: ethers.providers.Provider,
) {
const domain = this.resolveDomain(nameOrDomain);
super.registerProvider(domain, provider);
this.reconnect(domain);
}
registerSigner(nameOrDomain: string | number, signer: ethers.Signer) {
const domain = this.resolveDomain(nameOrDomain);
super.registerSigner(domain, signer);
this.reconnect(domain);
}
unregisterSigner(nameOrDomain: string | number): void {
const domain = this.resolveDomain(nameOrDomain);
super.unregisterSigner(domain);
this.reconnect(domain);
}
getCore(nameOrDomain: string | number): CoreContracts | undefined {
const domain = this.resolveDomain(nameOrDomain);
return this.cores.get(domain);
}
getBridge(nameOrDomain: string | number): BridgeContracts | undefined {
const domain = this.resolveDomain(nameOrDomain);
return this.bridges.get(domain);
}
// gets the replica of Home on Remote
getReplicaFor(
home: string | number,
remote: string | number,
): Replica | undefined {
return this.getCore(remote)?.replicas.get(this.resolveDomain(home))
?.contract;
}
// resolve the local repr of a token on its domain
async resolveTokenRepresentation(
nameOrDomain: string | number,
token: TokenIdentifier,
): Promise<ERC20 | undefined> {
const domain = this.resolveDomain(nameOrDomain);
const bridge = this.getBridge(domain);
const tokenDomain = this.resolveDomain(token.domain);
const tokenId = canonizeId(token.id);
const address = await bridge?.bridgeRouter[
'getLocalAddress(uint32,bytes32)'
](tokenDomain, tokenId);
if (!address) {
return;
}
let contract = new ERC20__factory().attach(address);
const connection = this.getConnection(domain);
if (connection) {
contract = contract.connect(connection);
}
return contract;
}
// resolve all token representations
async tokenRepresentations(
token: TokenIdentifier,
): Promise<ResolvedTokenInfo> {
const tokens: Map<number, ERC20> = new Map();
await Promise.all(
this.domainNumbers.map(async (domain) => {
let tok = await this.resolveTokenRepresentation(domain, token);
if (tok) {
tokens.set(domain, tok);
}
}),
);
return {
domain: this.resolveDomain(token.domain),
id: token.id,
tokens,
};
}
async resolveCanonicalToken(
nameOrDomain: string | number,
representation: Address,
): Promise<TokenIdentifier | undefined> {
const bridge = this.getBridge(nameOrDomain);
if (!bridge) {
throw new Error(`Bridge not available on ${nameOrDomain}`);
}
const token = await bridge.bridgeRouter.getCanonicalAddress(representation);
if (token[0] === 0) {
return;
}
return {
domain: token[0],
id: token[1],
};
}
// send tokens from domain to domain
async send(
from: string | number,
to: string | number,
token: TokenIdentifier,
amount: BigNumberish,
recipient: Address,
): Promise<ContractTransaction> {
const fromBridge = this.getBridge(from);
if (!fromBridge) {
throw new Error(`Bridge not available on ${from}`);
}
const fromToken = await this.resolveTokenRepresentation(from, token);
if (!fromToken) {
throw new Error(`Token not available on ${from}`);
}
const bridgeAddress = fromBridge?.bridgeRouter.address;
if (!bridgeAddress) {
throw new Error(`No bridge for ${from}`);
}
const sender = this.getSigner(from);
if (!sender) {
throw new Error(`No signer for ${from}`);
}
const senderAddress = await sender.getAddress();
const approved = await fromToken.allowance(senderAddress, bridgeAddress);
// Approve if necessary
if (approved.lt(amount)) {
await fromToken.approve(bridgeAddress, amount);
}
return fromBridge.bridgeRouter.send(
fromToken.address,
amount,
to,
recipient,
);
}
async sendNative(
from: string | number,
to: string | number,
amount: BigNumberish,
recipient: Address,
): Promise<ContractTransaction> {
const ethHelper = this.getBridge(from)?.ethHelper;
if (!ethHelper) {
throw new Error(`No ethHelper for ${from}`);
}
const toDomain = this.resolveDomain(to);
return ethHelper.sendToEVMLike(toDomain, recipient, { value: amount });
}
}
export const mainnet = OpticsContext.fromDomains(mainnetDomains);
export const dev = OpticsContext.fromDomains(devDomains);
export const staging = OpticsContext.fromDomains(stagingDomains);

@ -0,0 +1,67 @@
import fs from 'fs';
import { ethers } from 'ethers';
import {
BridgeRouter,
BridgeRouter__factory,
ETHHelper,
ETHHelper__factory,
} from '../../../../typechain/optics-xapps';
import { Contracts } from '../../contracts';
type Address = string;
export class BridgeContracts extends Contracts {
domain: number;
bridgeRouter: BridgeRouter;
ethHelper?: ETHHelper;
constructor(
domain: number,
br: Address,
ethHelper?: Address,
signer?: ethers.Signer,
) {
super(domain, br, ethHelper, signer);
this.domain = domain;
this.bridgeRouter = new BridgeRouter__factory(signer).attach(br);
if (ethHelper) {
this.ethHelper = new ETHHelper__factory(signer).attach(ethHelper);
}
}
connect(providerOrSigner: ethers.providers.Provider | ethers.Signer): void {
this.bridgeRouter = this.bridgeRouter.connect(providerOrSigner);
if (this.ethHelper) {
this.ethHelper = this.ethHelper.connect(providerOrSigner);
}
}
static fromObject(data: any, signer?: ethers.Signer) {
if (!data.domain || !data.bridgeRouter) {
throw new Error('missing address');
}
const domain = data.domain;
const br = data.bridgeRouter.proxy ?? data.bridgeRouter;
const eh = data.ethHelper;
return new BridgeContracts(domain, br, eh);
}
static loadJson(filepath: string, signer?: ethers.Signer) {
return this.fromObject(
JSON.parse(fs.readFileSync(filepath, 'utf8')),
signer,
);
}
toObject(): any {
const obj: any = {
bridgeRouter: this.bridgeRouter.address,
};
if (this.ethHelper) {
obj.ethHelper = this.ethHelper.address;
}
return obj;
}
}

@ -0,0 +1,81 @@
import fs from 'fs';
import { ethers } from 'ethers';
import {
Home,
Home__factory,
Replica,
Replica__factory,
} from '../../../../typechain/optics-core';
import { Contracts } from '../../contracts';
import { ReplicaInfo } from '../domains/domain';
type Address = string;
type InternalReplica = {
domain: number;
contract: Replica;
};
export class CoreContracts extends Contracts {
readonly domain;
home: Home;
replicas: Map<number, InternalReplica>;
constructor(
domain: number,
home: Address,
replicas: ReplicaInfo[],
signer?: ethers.Signer,
) {
super(domain, home, replicas, signer);
this.domain = domain;
this.home = new Home__factory(signer).attach(home);
this.replicas = new Map();
replicas.forEach((replica) => {
this.replicas.set(replica.domain, {
contract: new Replica__factory(signer).attach(replica.address),
domain: replica.domain,
});
});
}
connect(providerOrSigner: ethers.providers.Provider | ethers.Signer): void {
this.home = this.home.connect(providerOrSigner);
Array.from(this.replicas.values()).forEach((replica: InternalReplica) => {
replica.contract = replica.contract.connect(providerOrSigner);
});
}
toObject(): any {
const replicas: ReplicaInfo[] = Array.from(this.replicas.values()).map(
(replica) => {
return {
domain: replica.domain,
address: replica.contract.address,
};
},
);
return {
home: this.home.address,
replicas: replicas,
};
}
static fromObject(data: any, signer?: ethers.Signer): CoreContracts {
if (!data.domain || !data.home || !data.replicas) {
throw new Error('Missing key');
}
return new CoreContracts(data.domain, data.home, data.replicas, signer);
}
static loadJson(filepath: string, signer?: ethers.Signer) {
return this.fromObject(
JSON.parse(fs.readFileSync(filepath, 'utf8')),
signer,
);
}
}

@ -0,0 +1,47 @@
import { OpticsDomain } from './domain';
export const alfajores: OpticsDomain = {
name: 'alfajores',
domain: 1000,
bridgeRouter: '0xdaa6e362f9BE0CDaCe107b298639034b8dEC617a',
home: '0x47AaF05B1C36015eC186892C43ba4BaF91246aaA',
replicas: [
{ domain: 2000, address: '0x7804079cF55110dE7Db5aA67eB1Be00cBE9CA526' },
{
domain: 3000,
address: '0x6B8D6947B9b70f3ff1b547a15B969F625d28104a',
},
],
};
export const kovan: OpticsDomain = {
name: 'kovan',
domain: 3000,
bridgeRouter: '0x383Eb849c707fE38f3DfBF45679C0c6f21Ba82fF',
ethHelper: '0x6D84B823D7FB68E4d6f7Cc334fDd393f6C3a6980',
home: '0x5B55C29A10aEe6D5750F128C6a8f490de763ccc7',
replicas: [
{ domain: 2000, address: '0xC1AB4d72548Cc1C248EAdcD340035C3b213a47C3' },
{
domain: 1000,
address: '0xE63E73339501EE3A8d2928d6C88cf30aC8556Ee0',
},
],
};
export const rinkeby: OpticsDomain = {
name: 'rinkeby',
domain: 2000,
bridgeRouter: '0xE9fB0b6351Dec7d346282b8274653D36b8199AAF',
ethHelper: '0x7a539d7B7f4Acab1d7ce8b3681c3e286511ee444',
home: '0x6E6010E6bd43a9d2F7AE3b7eA9f61760e58758f3',
replicas: [
{ domain: 1000, address: '0x6A5F9531D1877ebE96Bc0631DbF64BBCf1f7421c' },
{
domain: 3000,
address: '0x6554bc7a5C35bA64Bf48FA8a9e662d8808aaa890',
},
],
};
export const devDomains = [alfajores, kovan, rinkeby];

@ -0,0 +1,14 @@
import { Domain } from '../../domains';
import { Address } from '../../utils';
export interface OpticsDomain extends Domain {
bridgeRouter: Address;
ethHelper?: Address;
home: Address;
replicas: ReplicaInfo[];
}
export interface ReplicaInfo {
domain: number;
address: Address;
}

@ -0,0 +1,4 @@
export { OpticsDomain, ReplicaInfo } from './domain';
export { mainnetDomains } from './mainnet';
export { devDomains } from './dev';
export { stagingDomains } from './staging';

@ -0,0 +1,50 @@
import { OpticsDomain } from './domain';
export const ethereum: OpticsDomain = {
name: 'ethereum',
domain: 6648936,
bridgeRouter: '0x6a39909e805A3eaDd2b61fFf61147796ca6aBB47',
ethHelper: '0xf1c1413096ff2278C3Df198a28F8D54e0369cF3A',
home: '0xf25C5932bb6EFc7afA4895D9916F2abD7151BF97',
replicas: [
{
domain: 1667591279,
address: '0x07b5B57b08202294E657D51Eb453A189290f6385',
},
{
domain: 1886350457,
address: '0x7725EadaC5Ee986CAc8317a1d2fB16e59e079E8b',
},
],
};
export const polygon: OpticsDomain = {
name: 'polygon',
domain: 1886350457,
bridgeRouter: '0xf244eA81F715F343040569398A4E7978De656bf6',
ethHelper: '0xc494bFEE14b5E1E118F93CfedF831f40dFA720fA',
home: '0x97bbda9A1D45D86631b243521380Bc070D6A4cBD',
replicas: [
{ domain: 6648936, address: '0xf25C5932bb6EFc7afA4895D9916F2abD7151BF97' },
{
domain: 1667591279,
address: '0x681Edb6d52138cEa8210060C309230244BcEa61b',
},
],
};
export const celo: OpticsDomain = {
name: 'celo',
domain: 1667591279,
bridgeRouter: '0xf244eA81F715F343040569398A4E7978De656bf6',
home: '0x97bbda9A1D45D86631b243521380Bc070D6A4cBD',
replicas: [
{ domain: 6648936, address: '0xf25c5932bb6efc7afa4895d9916f2abd7151bf97' },
{
domain: 1886350457,
address: '0x681Edb6d52138cEa8210060C309230244BcEa61b',
},
],
};
export const mainnetDomains = [ethereum, celo, polygon];

@ -0,0 +1,47 @@
import { OpticsDomain } from './domain';
export const alfajores: OpticsDomain = {
name: 'alfajores',
domain: 1000,
bridgeRouter: '0xd6930Ee55C141E5Bb4079d5963cF64320956bb3E',
home: '0x47AaF05B1C36015eC186892C43ba4BaF91246aaA',
replicas: [
{ domain: 2000, address: '0x7804079cF55110dE7Db5aA67eB1Be00cBE9CA526' },
{
domain: 3000,
address: '0x6B8D6947B9b70f3ff1b547a15B969F625d28104a',
},
],
};
export const kovan: OpticsDomain = {
name: 'kovan',
domain: 3000,
bridgeRouter: '0x359089D34687bDbFD019fCC5093fFC21bE9905f5',
ethHelper: '0x411ABcFD947212a0D64b97C9882556367b61704a',
home: '0x5B55C29A10aEe6D5750F128C6a8f490de763ccc7',
replicas: [
{ domain: 2000, address: '0xC1AB4d72548Cc1C248EAdcD340035C3b213a47C3' },
{
domain: 1000,
address: '0xE63E73339501EE3A8d2928d6C88cf30aC8556Ee0',
},
],
};
export const rinkeby: OpticsDomain = {
name: 'rinkeby',
domain: 2000,
bridgeRouter: '0x8FbEA25D0bFDbff68F2B920df180e9498E9c856A',
ethHelper: '0x1BEBC8F1260d16EE5d1CFEE9366bB474bD13DC5f',
home: '0x6E6010E6bd43a9d2F7AE3b7eA9f61760e58758f3',
replicas: [
{ domain: 1000, address: '0x6A5F9531D1877ebE96Bc0631DbF64BBCf1f7421c' },
{
domain: 3000,
address: '0x6554bc7a5C35bA64Bf48FA8a9e662d8808aaa890',
},
],
};
export const stagingDomains = [alfajores, kovan, rinkeby];

@ -0,0 +1,17 @@
export { BridgeContracts } from './contracts/BridgeContracts';
export { CoreContracts } from './contracts/CoreContracts';
export {
TransferMessage,
DetailsMessage,
RequestDetailsMesasage,
} from './messages/BridgeMessage';
export { OpticsMessage } from './messages/OpticsMessage';
export { ResolvedTokenInfo, TokenIdentifier } from './tokens';
export {
OpticsDomain,
mainnetDomains,
devDomains,
stagingDomains,
} from './domains';
export { OpticsContext, mainnet, dev, staging } from './OpticsContext';

@ -0,0 +1,166 @@
import { BigNumber } from '@ethersproject/bignumber';
import { arrayify, hexlify } from '@ethersproject/bytes';
import { BridgeContracts, CoreContracts, OpticsContext } from '..';
import { ResolvedTokenInfo, TokenIdentifier } from '../tokens';
import { DispatchEvent, OpticsMessage, parseMessage } from './OpticsMessage';
const ACTION_LEN = {
identifier: 1,
tokenId: 36,
transfer: 65,
details: 66,
requestDetails: 1,
};
type Transfer = {
action: 'transfer';
to: string;
amount: BigNumber;
};
export type Details = {
action: 'details';
name: string;
symbol: string;
decimals: number;
};
export type RequestDetails = { action: 'requestDetails' };
export type Action = Transfer | Details | RequestDetails;
export type ParsedBridgeMessage<T extends Action> = {
token: TokenIdentifier;
action: T;
};
export type ParsedTransferMessage = ParsedBridgeMessage<Transfer>;
export type ParsedDetailsMessage = ParsedBridgeMessage<Details>;
export type ParsedRequestDetailsMesasage = ParsedBridgeMessage<RequestDetails>;
function parseAction(buf: Uint8Array): Action {
if (buf.length === ACTION_LEN.requestDetails) {
return { action: 'requestDetails' };
}
// Transfer
if (buf.length === ACTION_LEN.transfer) {
// trim identifer
buf = buf.slice(ACTION_LEN.identifier);
return {
action: 'transfer',
to: hexlify(buf.slice(0, 32)),
amount: BigNumber.from(hexlify(buf.slice(32))),
};
}
// Details
if (buf.length === ACTION_LEN.details) {
// trim identifer
buf = buf.slice(ACTION_LEN.identifier);
// TODO(james): improve this to show real strings
return {
action: 'details',
name: hexlify(buf.slice(0, 32)),
symbol: hexlify(buf.slice(32, 64)),
decimals: buf[64],
};
}
throw new Error('Bad action');
}
function parseBody(
messageBody: string,
): ParsedTransferMessage | ParsedDetailsMessage | ParsedRequestDetailsMesasage {
const buf = arrayify(messageBody);
const tokenId = buf.slice(0, 36);
const token = {
domain: Buffer.from(tokenId).readUInt32BE(0),
id: hexlify(buf.slice(4)),
};
const action = parseAction(buf.slice(36));
const parsedMessage = {
action,
token,
};
switch (action.action) {
case 'transfer':
return parsedMessage as ParsedTransferMessage;
case 'details':
return parsedMessage as ParsedDetailsMessage;
case 'requestDetails':
return parsedMessage as ParsedRequestDetailsMesasage;
}
}
class BridgeMessage<T extends Action> extends OpticsMessage {
readonly token: TokenIdentifier;
readonly action: T;
readonly fromBridge: BridgeContracts;
readonly toBridge: BridgeContracts;
constructor(
event: DispatchEvent,
parsed: ParsedBridgeMessage<T>,
context: OpticsContext,
) {
super(event, context);
const fromBridge = context.getBridge(this.message.from);
const toBridge = context.getBridge(this.message.destination);
if (!fromBridge || !toBridge) {
throw new Error('missing bridge');
}
this.fromBridge = fromBridge;
this.toBridge = toBridge;
this.token = parsed.token;
this.action = parsed.action;
}
static fromEvent(
event: DispatchEvent,
context: OpticsContext,
): TransferMessage | DetailsMessage | RequestDetailsMesasage {
// kinda hate this but ok
const parsedEvent = parseMessage(event.args.message);
const parsed = parseBody(parsedEvent.body);
switch (parsed.action.action) {
case 'transfer':
return new BridgeMessage(
event,
parsed as ParsedTransferMessage,
context,
);
case 'details':
return new BridgeMessage(
event,
parsed as ParsedDetailsMessage,
context,
);
case 'requestDetails':
return new BridgeMessage(
event,
parsed as ParsedRequestDetailsMesasage,
context,
);
}
}
async asset(): Promise<ResolvedTokenInfo> {
return await this.context.tokenRepresentations(this.token);
}
}
export type TransferMessage = BridgeMessage<Transfer>;
export type DetailsMessage = BridgeMessage<Details>;
export type RequestDetailsMesasage = BridgeMessage<RequestDetails>;

@ -0,0 +1,106 @@
import { BigNumber, BigNumberish } from '@ethersproject/bignumber';
import { TypedEvent } from '../../../../typechain/optics-core/commons';
import { arrayify, hexlify } from '@ethersproject/bytes';
import { OpticsContext } from '..';
// match the typescript declaration
export type DispatchEvent = TypedEvent<
[string, BigNumber, BigNumber, string, string]
> & {
args: {
messageHash: string;
leafIndex: BigNumber;
destinationAndNonce: BigNumber;
committedRoot: string;
message: string;
};
};
export type ParsedMessage = {
from: number;
sender: string;
nonce: number;
destination: number;
recipient: string;
body: string;
};
enum MessageStatus {
None = 0,
Proven,
Processed,
}
export function parseMessage(message: string): ParsedMessage {
const buf = Buffer.from(arrayify(message));
const from = buf.readUInt32BE(0);
const sender = hexlify(buf.slice(4, 36));
const nonce = buf.readUInt32BE(36);
const destination = buf.readUInt32BE(40);
const recipient = hexlify(buf.slice(44, 76));
const body = hexlify(buf.slice(76));
return { from, sender, nonce, destination, recipient, body };
}
export class OpticsMessage {
readonly event: DispatchEvent;
readonly messageHash: string;
readonly leafIndex: BigNumber;
readonly destinationAndNonce: BigNumber;
readonly committedRoot: string;
readonly message: ParsedMessage;
protected context: OpticsContext;
constructor(event: DispatchEvent, context: OpticsContext) {
this.event = event;
this.messageHash = event.args.messageHash;
this.leafIndex = event.args.leafIndex;
this.destinationAndNonce = event.args.destinationAndNonce;
this.committedRoot = event.args.committedRoot;
this.message = parseMessage(event.args.message);
this.context = context;
}
async status(): Promise<MessageStatus> {
const replica = this.context.getReplicaFor(this.from, this.destination);
if (!replica) {
throw new Error(
`No replica on ${this.destination} for home ${this.from}`,
);
}
return await replica.messages(this.messageHash);
}
get from(): number {
return this.message.from;
}
get origin(): number {
return this.from;
}
get sender(): string {
return this.message.sender;
}
get nonce(): number {
return this.message.nonce;
}
get destination(): number {
return this.message.destination;
}
get recipient(): string {
return this.message.recipient;
}
get body(): string {
return this.message.body;
}
}

@ -0,0 +1,34 @@
import * as ethers from 'ethers';
import { mainnet } from '.';
const celoTokenAddr = '0x471EcE3750Da237f93B8E339c536989b8978a438';
const amount = ethers.constants.WeiPerEther.mul(100);
const privkey = process.env.PRIVKEY_LMAO;
if (!privkey) {
throw new Error('set PRIVKEY_LMAO');
}
const celoRpc = 'https://forno.celo.org';
mainnet.registerRpcProvider('celo', celoRpc);
mainnet.registerWalletSigner('celo', privkey);
async function doThing() {
const address = await mainnet.getAddress('celo');
if (!address) {
throw new Error('no address');
}
const tx = await mainnet.send(
'celo',
'ethereum',
{ domain: 'celo', id: celoTokenAddr },
amount,
address,
);
console.log(`sendTx is ${tx.hash}`);
await tx.wait(1);
}
doThing();

@ -0,0 +1,13 @@
import { BytesLike } from 'ethers';
import { ERC20 } from '../../../typechain/optics-xapps';
export interface TokenIdentifier {
domain: string | number;
id: BytesLike;
}
export type ResolvedTokenInfo = {
domain: number;
id: BytesLike;
tokens: Map<number, ERC20>;
};

@ -0,0 +1,110 @@
import * as ethers from 'ethers';
import { Domain } from './domains';
import { mainnetDomains } from './optics/domains/mainnet';
type Provider = ethers.providers.Provider;
export class MultiProvider {
private domains: Map<number, Domain>;
private providers: Map<number, Provider>;
private signers: Map<number, ethers.Signer>;
constructor() {
this.domains = new Map();
this.providers = new Map();
this.signers = new Map();
}
registerDomain(domain: Domain) {
this.domains.set(domain.domain, domain);
}
getDomain(domain: number): Domain | undefined {
return this.domains.get(domain);
}
get domainNumbers(): number[] {
return Array.from(this.domains.keys());
}
resolveDomain(nameOrDomain: string | number): number {
if (typeof nameOrDomain === 'string') {
return Array.from(this.domains.values()).filter(
(domain) => domain.name === nameOrDomain,
)[0].domain;
} else {
return nameOrDomain;
}
}
registerProvider(nameOrDomain: string | number, provider: Provider) {
const domain = this.resolveDomain(nameOrDomain);
if (!this.domains.get(domain)) {
throw new Error('Must have domain to register provider');
}
this.providers.set(domain, provider);
const signer = this.signers.get(domain);
if (signer) {
this.signers.set(domain, signer.connect(provider));
}
}
registerRpcProvider(nameOrDomain: string | number, rpc: string) {
const domain = this.resolveDomain(nameOrDomain);
const provider = new ethers.providers.JsonRpcProvider(rpc);
this.registerProvider(domain, provider);
}
getProvider(nameOrDomain: string | number): Provider | undefined {
const domain = this.resolveDomain(nameOrDomain);
return this.providers.get(domain);
}
registerSigner(nameOrDomain: string | number, signer: ethers.Signer) {
const domain = this.resolveDomain(nameOrDomain);
const provider = this.providers.get(domain);
if (!provider && !signer.provider) {
throw new Error('Must have a provider before registering signer');
}
if (provider) {
this.signers.set(domain, signer.connect(provider));
} else {
this.registerProvider(domain, signer.provider!);
this.signers.set(domain, signer);
}
}
unregisterSigner(nameOrDomain: string | number) {
this.signers.delete(this.resolveDomain(nameOrDomain));
}
registerWalletSigner(nameOrDomain: string | number, privkey: string) {
const domain = this.resolveDomain(nameOrDomain);
const wallet = new ethers.Wallet(privkey);
this.registerSigner(domain, wallet);
}
getSigner(nameOrDomain: string | number): ethers.Signer | undefined {
const domain = this.resolveDomain(nameOrDomain);
return this.signers.get(domain);
}
getConnection(
nameOrDomain: string | number,
): ethers.Signer | ethers.providers.Provider | undefined {
return this.getSigner(nameOrDomain) ?? this.getProvider(nameOrDomain);
}
async getAddress(nameOrDomain: string | number): Promise<string | undefined> {
const signer = this.getSigner(nameOrDomain);
return await signer?.getAddress();
}
}

@ -0,0 +1,16 @@
import { BytesLike } from '@ethersproject/bytes';
import { ethers } from 'ethers';
export type Address = string;
// ensure that a bytes-like is 32 long. left-pad with 0s if not
export function canonizeId(data: BytesLike): Uint8Array {
const buf = ethers.utils.arrayify(data);
if (buf.length > 32) {
throw new Error('Too long');
}
if (buf.length !== 20 && buf.length != 32) {
throw new Error('bad input, expect address or bytes32');
}
return ethers.utils.zeroPad(buf, 32);
}

@ -0,0 +1,72 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
Loading…
Cancel
Save