feat/refactor: Multi-Provider improvements (#842)

* refactor: instantiate OpticsMessage with event instead of transaction receipt

* refactor: take event AND transaction receipt; add convenience getters

* Move sendCoins to examples folder

* Remove unused functions from contracts

* Add mustGetReplica

* feat: MultiEvents class

* feat: events function

* Add comments for confirmAt

* Add enum for MessageStatus

* refactor: remove MultiEvent class

* fix: return only successfully parsed bridge messages

* nit: import

* refactor: get functions

* cache events to OpticsMessage

* feat: implement RichEvents

* Add allTransactionLogs to RichEvent

* final polish

refactor: make event queries rely on ethers type system

refactor: generic Annotated type

refactor: improve event cache in Optics message

refactor: non-looping array concat

refactor: use Annotated events for message instantiation

refactor: add concurrency limit to paginated event retrieval

refactor: break up annotate and rely on event.getTransactionReceipt

refactor: use annotated lifecycle events throughout

refactor: break out events into a folder

bug: check for id, not domain

bug: check for id, not domain (again)

bug: check for id, not domain (again)

Revert "bug: check for id, not domain (again)"

This reverts commit 8304985814bed4b254082187ff8d303b5b517c4c.

bug: pass through signer in fromObject

bug: improper fromObject checking re: domain

refactor: break trace out into its own package

remove .env

* fix: remove tsbuildinfo

* fix: add additional context to optics message event

Co-authored-by: James Prestwich <james@prestwi.ch>
buddies-main-deployment
Anna Carroll 3 years ago committed by GitHub
parent f532a18d3b
commit c146d76ee7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitignore
  2. 1
      typescript/optics-provider/.gitignore
  3. 38
      typescript/optics-provider/package-lock.json
  4. 7
      typescript/optics-provider/package.json
  5. 13
      typescript/optics-provider/src/contracts.ts
  6. 8
      typescript/optics-provider/src/domains.ts
  7. 12
      typescript/optics-provider/src/index.ts
  8. 15
      typescript/optics-provider/src/optics/OpticsContext.ts
  9. 16
      typescript/optics-provider/src/optics/contracts/BridgeContracts.ts
  10. 13
      typescript/optics-provider/src/optics/contracts/CoreContracts.ts
  11. 6
      typescript/optics-provider/src/optics/domains/dev.ts
  12. 10
      typescript/optics-provider/src/optics/domains/mainnet.ts
  13. 6
      typescript/optics-provider/src/optics/domains/staging.ts
  14. 26
      typescript/optics-provider/src/optics/events/bridgeEvents.ts
  15. 146
      typescript/optics-provider/src/optics/events/fetch.ts
  16. 50
      typescript/optics-provider/src/optics/events/index.ts
  17. 44
      typescript/optics-provider/src/optics/events/opticsEvents.ts
  18. 2
      typescript/optics-provider/src/optics/examples/sendCoins.ts
  19. 26
      typescript/optics-provider/src/optics/index.ts
  20. 145
      typescript/optics-provider/src/optics/messages/BridgeMessage.ts
  21. 339
      typescript/optics-provider/src/optics/messages/OpticsMessage.ts
  22. 2
      typescript/optics-provider/src/optics/tokens/testnetWellKnown.ts
  23. 44
      typescript/optics-provider/src/provider.ts
  24. 3
      typescript/optics-provider/tsconfig.json
  25. 1
      typescript/optics-provider/tsconfig.tsbuildinfo
  26. 2
      typescript/tsconfig.package.json

2
.gitignore vendored

@ -2,6 +2,8 @@ node_modules
test_deploy.env
typescript/optics-deploy/.env
**/.env
**/tsconfig.tsbuildinfo
rust/vendor/
rust/tmp_db

@ -1,2 +1,3 @@
.env
dist
tsconfig.tsbuildinfo

@ -15,7 +15,9 @@
"devDependencies": {
"@types/node": "^16.9.1",
"dotenv": "^10.0.0",
"fs": "0.0.1-security"
"fs": "0.0.1-security",
"tsc": "^2.0.3",
"typescript": "^4.4.3"
}
},
"node_modules/@ethersproject/abi": {
@ -860,6 +862,28 @@
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
},
"node_modules/tsc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.3.tgz",
"integrity": "sha512-SN+9zBUtrpUcOpaUO7GjkEHgWtf22c7FKbKCA4e858eEM7Qz86rRDpgOU2lBIDf0fLCsEg65ms899UMUIB2+Ow==",
"dev": true,
"bin": {
"tsc": "bin/tsc"
}
},
"node_modules/typescript": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",
@ -1401,6 +1425,18 @@
"resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
"integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA=="
},
"tsc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tsc/-/tsc-2.0.3.tgz",
"integrity": "sha512-SN+9zBUtrpUcOpaUO7GjkEHgWtf22c7FKbKCA4e858eEM7Qz86rRDpgOU2lBIDf0fLCsEg65ms899UMUIB2+Ow==",
"dev": true
},
"typescript": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==",
"dev": true
},
"ws": {
"version": "7.4.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz",

@ -6,7 +6,8 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"check": "tsc --noEmit"
"check": "tsc --noEmit",
"prettier": "prettier --write ./src"
},
"prepublish": "tsc",
"author": "Celo Labs Inc.",
@ -14,7 +15,9 @@
"devDependencies": {
"@types/node": "^16.9.1",
"dotenv": "^10.0.0",
"fs": "0.0.1-security"
"fs": "0.0.1-security",
"tsc": "^2.0.3",
"typescript": "^4.4.3"
},
"dependencies": {
"@optics-xyz/ts-interface": "^1.0.9",

@ -1,5 +1,4 @@
import { ethers } from 'ethers';
import fs from 'fs';
export abstract class Contracts {
readonly args: any;
@ -11,16 +10,4 @@ export abstract class Contracts {
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());
}
}

@ -1,4 +1,10 @@
export interface Pagination {
blocks: number;
from: number;
}
export interface Domain {
domain: number;
id: number;
name: string;
paginate?: Pagination;
}

@ -1,6 +1,14 @@
export { MultiProvider } from './provider';
export { mainnet, dev, staging, OpticsContext } from './optics';
export {
mainnet,
dev,
staging,
OpticsContext,
OpticsStatus,
OpticsMessage,
OpticsLifecyleEvent,
Annotated,
} from './optics';
// intended usage
// import {mainnet} from 'optics-provider';

@ -115,6 +115,17 @@ export class OpticsContext extends MultiProvider {
?.contract;
}
mustGetReplicaFor(
home: string | number,
remote: string | number,
): core.Replica {
const replica = this.getReplicaFor(home, remote);
if (!replica) {
throw new Error(`Missing replica for home ${home} & remote ${remote}`);
}
return replica;
}
// resolve the local repr of a token on its domain
async resolveTokenRepresentation(
nameOrDomain: string | number,
@ -218,7 +229,7 @@ export class OpticsContext extends MultiProvider {
);
const receipt = await tx.wait();
const message = TransferMessage.fromReceipt(receipt, this);
const message = TransferMessage.singleFromReceipt(this, from, receipt);
if (!message) {
throw new Error();
}
@ -245,7 +256,7 @@ export class OpticsContext extends MultiProvider {
const tx = await ethHelper.sendToEVMLike(toDomain, recipient, overrides);
const receipt = await tx.wait();
const message = TransferMessage.fromReceipt(receipt, this);
const message = TransferMessage.singleFromReceipt(this, from, receipt);
if (!message) {
throw new Error();
}

@ -1,4 +1,3 @@
import fs from 'fs';
import { ethers } from 'ethers';
import { xapps } from '@optics-xyz/ts-interface';
import { Contracts } from '../../contracts';
@ -32,22 +31,15 @@ export class BridgeContracts extends Contracts {
}
static fromObject(data: any, signer?: ethers.Signer) {
if (!data.domain || !data.bridgeRouter) {
throw new Error('missing address');
if (!data.id || !data.bridgeRouter) {
throw new Error('missing address or domain');
}
const domain = data.domain;
const id = data.id;
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,
);
return new BridgeContracts(id, br, eh, signer);
}
toObject(): any {

@ -1,5 +1,3 @@
import fs from 'fs';
import { ethers } from 'ethers';
import { core } from '@optics-xyz/ts-interface';
import { Contracts } from '../../contracts';
@ -61,16 +59,9 @@ export class CoreContracts extends Contracts {
}
static fromObject(data: any, signer?: ethers.Signer): CoreContracts {
if (!data.domain || !data.home || !data.replicas) {
if (!data.id || !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,
);
return new CoreContracts(data.id, data.home, data.replicas, signer);
}
}

@ -2,7 +2,7 @@ import { OpticsDomain } from './domain';
export const alfajores: OpticsDomain = {
name: 'alfajores',
domain: 1000,
id: 1000,
bridgeRouter: '0xdaa6e362f9BE0CDaCe107b298639034b8dEC617a',
home: '0x47AaF05B1C36015eC186892C43ba4BaF91246aaA',
replicas: [
@ -16,7 +16,7 @@ export const alfajores: OpticsDomain = {
export const kovan: OpticsDomain = {
name: 'kovan',
domain: 3000,
id: 3000,
bridgeRouter: '0x383Eb849c707fE38f3DfBF45679C0c6f21Ba82fF',
ethHelper: '0x6D84B823D7FB68E4d6f7Cc334fDd393f6C3a6980',
home: '0x5B55C29A10aEe6D5750F128C6a8f490de763ccc7',
@ -31,7 +31,7 @@ export const kovan: OpticsDomain = {
export const rinkeby: OpticsDomain = {
name: 'rinkeby',
domain: 2000,
id: 2000,
bridgeRouter: '0xE9fB0b6351Dec7d346282b8274653D36b8199AAF',
ethHelper: '0x7a539d7B7f4Acab1d7ce8b3681c3e286511ee444',
home: '0x6E6010E6bd43a9d2F7AE3b7eA9f61760e58758f3',

@ -2,7 +2,7 @@ import { OpticsDomain } from './domain';
export const ethereum: OpticsDomain = {
name: 'ethereum',
domain: 6648936,
id: 6648936,
bridgeRouter: '0x6a39909e805A3eaDd2b61fFf61147796ca6aBB47',
ethHelper: '0xf1c1413096ff2278C3Df198a28F8D54e0369cF3A',
home: '0xf25C5932bb6EFc7afA4895D9916F2abD7151BF97',
@ -20,7 +20,11 @@ export const ethereum: OpticsDomain = {
export const polygon: OpticsDomain = {
name: 'polygon',
domain: 1886350457,
id: 1886350457,
paginate: {
blocks: 1999,
from: 18895794,
},
bridgeRouter: '0xf244eA81F715F343040569398A4E7978De656bf6',
ethHelper: '0xc494bFEE14b5E1E118F93CfedF831f40dFA720fA',
home: '0x97bbda9A1D45D86631b243521380Bc070D6A4cBD',
@ -35,7 +39,7 @@ export const polygon: OpticsDomain = {
export const celo: OpticsDomain = {
name: 'celo',
domain: 1667591279,
id: 1667591279,
bridgeRouter: '0xf244eA81F715F343040569398A4E7978De656bf6',
home: '0x97bbda9A1D45D86631b243521380Bc070D6A4cBD',
replicas: [

@ -2,7 +2,7 @@ import { OpticsDomain } from './domain';
export const alfajores: OpticsDomain = {
name: 'alfajores',
domain: 1000,
id: 1000,
bridgeRouter: '0xd6930Ee55C141E5Bb4079d5963cF64320956bb3E',
home: '0x47AaF05B1C36015eC186892C43ba4BaF91246aaA',
replicas: [
@ -16,7 +16,7 @@ export const alfajores: OpticsDomain = {
export const kovan: OpticsDomain = {
name: 'kovan',
domain: 3000,
id: 3000,
bridgeRouter: '0x359089D34687bDbFD019fCC5093fFC21bE9905f5',
ethHelper: '0x411ABcFD947212a0D64b97C9882556367b61704a',
home: '0x5B55C29A10aEe6D5750F128C6a8f490de763ccc7',
@ -31,7 +31,7 @@ export const kovan: OpticsDomain = {
export const rinkeby: OpticsDomain = {
name: 'rinkeby',
domain: 2000,
id: 2000,
bridgeRouter: '0x8FbEA25D0bFDbff68F2B920df180e9498E9c856A',
ethHelper: '0x1BEBC8F1260d16EE5d1CFEE9366bB474bD13DC5f',
home: '0x6E6010E6bd43a9d2F7AE3b7eA9f61760e58758f3',

@ -0,0 +1,26 @@
import { TypedEvent } from '@optics-xyz/ts-interface/dist/optics-core/commons';
import { BigNumber } from 'ethers';
import { Annotated } from '.';
export type SendTypes = [string, string, number, string, BigNumber];
export type SendArgs = {
token: string;
from: string;
toDomain: number;
toId: string;
amount: BigNumber;
};
export type SendEvent = TypedEvent<SendTypes & SendArgs>;
export type TokenDeployedTypes = [number, string, string];
export type TokenDeployedArgs = {
domain: number;
id: string;
representation: string;
};
export type TokenDeployedEvent = TypedEvent<
TokenDeployedTypes & TokenDeployedArgs
>;
export type AnnotatedSend = Annotated<SendEvent>;
export type AnnotatedTokenDeployed = Annotated<TokenDeployedEvent>;

@ -0,0 +1,146 @@
import { Annotated } from '.';
import { OpticsContext } from '..';
import { Domain } from '../../domains';
import { TransactionReceipt } from '@ethersproject/abstract-provider';
import { Result } from '@ethersproject/abi';
import {
TypedEvent,
TypedEventFilter,
} from '@optics-xyz/ts-interface/dist/optics-core/commons';
// specifies an interface shared by the TS generated contracts
export interface TSContract<T extends Result, U> {
queryFilter(
event: TypedEventFilter<T, U>,
fromBlockOrBlockhash?: string | number | undefined,
toBlock?: string | number | undefined,
): Promise<Array<TypedEvent<T & U>>>;
}
export function annotate<U extends Result, T extends TypedEvent<U>>(
domain: number,
receipt: TransactionReceipt,
event: T,
): Annotated<T> {
return {
domain,
receipt,
name: event.eventSignature?.split('(')[0],
event,
};
}
export async function annotateEvent<U extends Result, T extends TypedEvent<U>>(
domain: number,
event: T,
): Promise<Annotated<T>> {
const receipt = await event.getTransactionReceipt();
return annotate(domain, receipt, event);
}
export async function annotateEvents<U extends Result, T extends TypedEvent<U>>(
domain: number,
events: T[],
): Promise<Annotated<T>[]> {
return await Promise.all(
events.map(async (event) => annotateEvent(domain, event)),
);
}
export async function queryAnnotatedEvents<T extends Result, U>(
context: OpticsContext,
nameOrDomain: string | number,
contract: TSContract<T, U>,
filter: TypedEventFilter<T, U>,
startBlock?: number,
endBlock?: number,
): Promise<Array<Annotated<TypedEvent<T & U>>>> {
const events = await getEvents(
context,
nameOrDomain,
contract,
filter,
startBlock,
endBlock,
);
return await annotateEvents(context.resolveDomain(nameOrDomain), events);
}
export async function getEvents<T extends Result, U>(
context: OpticsContext,
nameOrDomain: string | number,
contract: TSContract<T, U>,
filter: TypedEventFilter<T, U>,
startBlock?: number,
endBlock?: number,
): Promise<Array<TypedEvent<T & U>>> {
const domain = context.mustGetDomain(nameOrDomain);
if (domain.paginate) {
return getPaginatedEvents(
context,
domain,
contract,
filter,
startBlock,
endBlock,
);
}
return contract.queryFilter(filter, startBlock, endBlock);
}
export async function getPaginatedEvents<T extends Result, U>(
context: OpticsContext,
domain: Domain,
contract: TSContract<T, U>,
filter: TypedEventFilter<T, U>,
startBlock?: number,
endBlock?: number,
): Promise<Array<TypedEvent<T & U>>> {
// get the first block by params
// or domain deployment block
const firstBlock = startBlock ? startBlock : domain.paginate!.from;
// get the last block by params
// or current block number
let lastBlock;
if (!endBlock) {
const provider = context.mustGetProvider(domain.id);
lastBlock = await provider.getBlockNumber();
} else {
lastBlock = endBlock;
}
// query domain pagination limit at a time, concurrently
const callArgs = [];
for (
let from = firstBlock;
from < lastBlock;
from += domain.paginate!.blocks
) {
let nextPage = from + domain.paginate!.blocks;
let to = nextPage > lastBlock ? lastBlock : nextPage;
callArgs.push({ filter, from, to });
}
const eventArrayPromises = [];
for (
let currStartBlock = firstBlock;
currStartBlock < lastBlock;
currStartBlock += domain.paginate!.blocks
) {
let attemptedEndBlock = currStartBlock + domain.paginate!.blocks;
let currEndBlock =
attemptedEndBlock > lastBlock ? lastBlock : attemptedEndBlock;
const eventArrayPromise = contract.queryFilter(
filter,
currStartBlock,
currEndBlock,
);
eventArrayPromises.push(eventArrayPromise);
}
// await promises & concatenate results
const eventArrays = await Promise.all(eventArrayPromises);
let events: Array<TypedEvent<T & U>> = [];
for (let eventArray of eventArrays) {
events = events.concat(eventArray);
}
return events;
}

@ -0,0 +1,50 @@
import { TransactionReceipt } from '@ethersproject/abstract-provider';
export type {
AnnotatedDispatch,
AnnotatedUpdate,
AnnotatedProcess,
AnnotatedLifecycleEvent,
OpticsLifecyleEvent,
DispatchEvent,
ProcessEvent,
UpdateEvent,
UpdateArgs,
UpdateTypes,
ProcessArgs,
ProcessTypes,
DispatchArgs,
DispatchTypes,
} from './opticsEvents';
export type {
SendTypes,
SendArgs,
SendEvent,
TokenDeployedTypes,
TokenDeployedArgs,
TokenDeployedEvent,
AnnotatedSend,
AnnotatedTokenDeployed,
} from './bridgeEvents';
export type Annotated<T> = {
// What domain did it occur on?
domain: number;
// Receipt for the tx where it occurred
receipt: TransactionReceipt;
// event name
name?: string;
// The event
event: T;
};
export {
queryAnnotatedEvents,
annotate,
annotateEvent,
annotateEvents,
} from './fetch';

@ -0,0 +1,44 @@
import { BigNumber } from '@ethersproject/bignumber';
import { TypedEvent } from '@optics-xyz/ts-interface/dist/optics-core/commons';
import { Annotated } from '.';
// copied from the Home.d.ts
export type DispatchTypes = [string, BigNumber, BigNumber, string, string];
export type DispatchArgs = {
messageHash: string;
leafIndex: BigNumber;
destinationAndNonce: BigNumber;
committedRoot: string;
message: string;
};
export type DispatchEvent = TypedEvent<DispatchTypes & DispatchArgs>;
// copied from the Home.d.ts
export type UpdateTypes = [number, string, string, string];
export type UpdateArgs = {
homeDomain: number;
oldRoot: string;
newRoot: string;
signature: string;
};
export type UpdateEvent = TypedEvent<UpdateTypes & UpdateArgs>;
// copied from the Replica.d.ts
export type ProcessTypes = [string, boolean, string];
export type ProcessArgs = {
messageHash: string;
success: boolean;
returnData: string;
};
export type ProcessEvent = TypedEvent<ProcessTypes & ProcessArgs>;
export type OpticsLifecyleEvent = ProcessEvent | UpdateEvent | DispatchEvent;
export type AnnotatedDispatch = Annotated<DispatchEvent>;
export type AnnotatedUpdate = Annotated<UpdateEvent>;
export type AnnotatedProcess = Annotated<ProcessEvent>;
export type AnnotatedLifecycleEvent =
| AnnotatedDispatch
| AnnotatedUpdate
| AnnotatedProcess;

@ -1,6 +1,6 @@
import * as ethers from 'ethers';
import { mainnet } from '.';
import { mainnet } from '..';
const celoTokenAddr = '0x471EcE3750Da237f93B8E339c536989b8978a438';

@ -1,18 +1,34 @@
export { BridgeContracts } from './contracts/BridgeContracts';
export { CoreContracts } from './contracts/CoreContracts';
export {
TransferMessage,
DetailsMessage,
RequestDetailsMessage,
} from './messages/BridgeMessage';
export { OpticsMessage } from './messages/OpticsMessage';
export {
OpticsMessage,
OpticsStatus,
MessageStatus,
} from './messages/OpticsMessage';
export type { ResolvedTokenInfo, TokenIdentifier } from './tokens';
export { tokens, testnetTokens } from './tokens';
export type { OpticsDomain } from './domains';
export { mainnetDomains, devDomains, stagingDomains } from './domains';
export type {
AnnotatedLifecycleEvent,
OpticsLifecyleEvent,
Annotated,
} from './events';
export {
mainnetDomains,
devDomains,
stagingDomains,
} from './domains';
queryAnnotatedEvents,
annotate,
annotateEvent,
annotateEvents,
} from './events';
export { OpticsContext, mainnet, dev, staging } from './OpticsContext';

@ -1,10 +1,12 @@
import { BigNumber } from '@ethersproject/bignumber';
import { arrayify, hexlify } from '@ethersproject/bytes';
import { ContractReceipt, ethers } from 'ethers';
import { BridgeContracts, OpticsContext } from '..';
import { TransactionReceipt } from '@ethersproject/abstract-provider';
import { ethers } from 'ethers';
import { xapps } from '@optics-xyz/ts-interface';
import { BridgeContracts, OpticsContext } from '..';
import { ResolvedTokenInfo, TokenIdentifier } from '../tokens';
import { OpticsMessage, parseMessage } from './OpticsMessage';
import { OpticsMessage } from './OpticsMessage';
import { AnnotatedDispatch } from '../events/opticsEvents';
const ACTION_LEN = {
identifier: 1,
@ -15,19 +17,19 @@ const ACTION_LEN = {
};
type Transfer = {
action: 'transfer';
type: 'transfer';
to: string;
amount: BigNumber;
};
export type Details = {
action: 'details';
type: 'details';
name: string;
symbol: string;
decimals: number;
};
export type RequestDetails = { action: 'requestDetails' };
export type RequestDetails = { type: 'requestDetails' };
export type Action = Transfer | Details | RequestDetails;
@ -36,13 +38,17 @@ export type ParsedBridgeMessage<T extends Action> = {
action: T;
};
export type AnyBridgeMessage =
| TransferMessage
| DetailsMessage
| RequestDetailsMessage;
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' };
return { type: 'requestDetails' };
}
// Transfer
@ -50,7 +56,7 @@ function parseAction(buf: Uint8Array): Action {
// trim identifer
buf = buf.slice(ACTION_LEN.identifier);
return {
action: 'transfer',
type: 'transfer',
to: hexlify(buf.slice(0, 32)),
amount: BigNumber.from(hexlify(buf.slice(32))),
};
@ -62,8 +68,7 @@ function parseAction(buf: Uint8Array): Action {
buf = buf.slice(ACTION_LEN.identifier);
// TODO(james): improve this to show real strings
return {
action: 'details',
type: 'details',
name: hexlify(buf.slice(0, 32)),
symbol: hexlify(buf.slice(32, 64)),
decimals: buf[64],
@ -90,7 +95,7 @@ function parseBody(
token,
};
switch (action.action) {
switch (action.type) {
case 'transfer':
return parsedMessage as ParsedTransferMessage;
case 'details':
@ -102,20 +107,19 @@ function parseBody(
class BridgeMessage extends OpticsMessage {
readonly token: TokenIdentifier;
readonly fromBridge: BridgeContracts;
readonly toBridge: BridgeContracts;
constructor(
receipt: ContractReceipt,
token: TokenIdentifier,
context: OpticsContext,
event: AnnotatedDispatch,
token: TokenIdentifier,
callerKnowsWhatTheyAreDoing: boolean,
) {
if (!callerKnowsWhatTheyAreDoing) {
throw new Error('Use `fromReceipt` to instantiate');
}
super(receipt, context);
super(context, event);
const fromBridge = context.mustGetBridge(this.message.from);
const toBridge = context.mustGetBridge(this.message.destination);
@ -125,40 +129,99 @@ class BridgeMessage extends OpticsMessage {
this.token = token;
}
static fromReceipt(
receipt: ethers.ContractReceipt,
static fromOpticsMessage(
context: OpticsContext,
): TransferMessage | DetailsMessage | RequestDetailsMessage {
// kinda hate this but ok
const oMessage = new OpticsMessage(receipt, context);
let event = oMessage.event;
const parsedEvent = parseMessage(event.args.message);
const parsed = parseBody(parsedEvent.body);
opticsMessage: OpticsMessage,
): AnyBridgeMessage {
const parsedMessageBody = parseBody(opticsMessage.message.body);
switch (parsed.action.action) {
switch (parsedMessageBody.action.type) {
case 'transfer':
return new TransferMessage(
receipt,
parsed as ParsedTransferMessage,
context,
opticsMessage.dispatch,
parsedMessageBody as ParsedTransferMessage,
);
case 'details':
return new DetailsMessage(
receipt,
parsed as ParsedDetailsMessage,
context,
opticsMessage.dispatch,
parsedMessageBody as ParsedDetailsMessage,
);
case 'requestDetails':
return new RequestDetailsMessage(
receipt,
parsed as ParsedRequestDetailsMesasage,
context,
opticsMessage.dispatch,
parsedMessageBody as ParsedRequestDetailsMesasage,
);
}
}
static fromReceipt(
context: OpticsContext,
nameOrDomain: string | number,
receipt: TransactionReceipt,
): AnyBridgeMessage[] {
const opticsMessages: OpticsMessage[] = OpticsMessage.fromReceipt(
context,
nameOrDomain,
receipt,
);
const bridgeMessages: AnyBridgeMessage[] = [];
for (let opticsMessage of opticsMessages) {
try {
const bridgeMessage = BridgeMessage.fromOpticsMessage(
context,
opticsMessage,
);
bridgeMessages.push(bridgeMessage);
} catch (e) {}
}
return bridgeMessages;
}
static singleFromReceipt(
context: OpticsContext,
nameOrDomain: string | number,
receipt: TransactionReceipt,
): AnyBridgeMessage {
const messages: AnyBridgeMessage[] = BridgeMessage.fromReceipt(
context,
nameOrDomain,
receipt,
);
if (messages.length !== 1) {
throw new Error('Expected single Dispatch in transaction');
}
return messages[0];
}
static async fromTransactionHash(
context: OpticsContext,
nameOrDomain: string | number,
transactionHash: string,
): Promise<AnyBridgeMessage[]> {
const provider = context.mustGetProvider(nameOrDomain);
const receipt = await provider.getTransactionReceipt(transactionHash);
if (!receipt) {
throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`);
}
return BridgeMessage.fromReceipt(context, nameOrDomain, receipt);
}
static async singleFromTransactionHash(
context: OpticsContext,
nameOrDomain: string | number,
transactionHash: string,
): Promise<AnyBridgeMessage> {
const provider = context.mustGetProvider(nameOrDomain);
const receipt = await provider.getTransactionReceipt(transactionHash);
if (!receipt) {
throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`);
}
return BridgeMessage.singleFromReceipt(context, nameOrDomain, receipt!);
}
async asset(): Promise<ResolvedTokenInfo> {
return await this.context.tokenRepresentations(this.token);
}
@ -178,11 +241,11 @@ export class TransferMessage extends BridgeMessage {
action: Transfer;
constructor(
receipt: ContractReceipt,
parsed: ParsedTransferMessage,
context: OpticsContext,
event: AnnotatedDispatch,
parsed: ParsedTransferMessage,
) {
super(receipt, parsed.token, context, true);
super(context, event, parsed.token, true);
this.action = parsed.action;
}
@ -210,11 +273,11 @@ export class DetailsMessage extends BridgeMessage {
action: Details;
constructor(
receipt: ContractReceipt,
parsed: ParsedDetailsMessage,
context: OpticsContext,
event: AnnotatedDispatch,
parsed: ParsedDetailsMessage,
) {
super(receipt, parsed.token, context, true);
super(context, event, parsed.token, true);
this.action = parsed.action;
}
@ -235,11 +298,11 @@ export class RequestDetailsMessage extends BridgeMessage {
action: RequestDetails;
constructor(
receipt: ContractReceipt,
parsed: ParsedRequestDetailsMesasage,
context: OpticsContext,
event: AnnotatedDispatch,
parsed: ParsedRequestDetailsMesasage,
) {
super(receipt, parsed.token, context, true);
super(context, event, parsed.token, true);
this.action = parsed.action;
}
}

@ -1,22 +1,23 @@
import { LogDescription } from '@ethersproject/abi';
import { BigNumber } from '@ethersproject/bignumber';
import { arrayify, hexlify } from '@ethersproject/bytes';
import { ContractReceipt } from '@ethersproject/contracts';
import { OpticsContext } from '..';
import { TransactionReceipt } from '@ethersproject/abstract-provider';
import { core } from '@optics-xyz/ts-interface';
import { OpticsContext } from '..';
import { delay } from '../../utils';
import {
DispatchEvent,
AnnotatedDispatch,
annotate,
AnnotatedUpdate,
AnnotatedProcess,
UpdateTypes,
UpdateArgs,
ProcessTypes,
ProcessArgs,
AnnotatedLifecycleEvent,
} from '../events';
// match the typescript declaration
export interface DispatchEvent {
transactionHash: string;
args: {
messageHash: string;
leafIndex: BigNumber;
destinationAndNonce: BigNumber;
committedRoot: string;
message: string;
};
}
import { queryAnnotatedEvents } from '..';
export type ParsedMessage = {
from: number;
@ -27,84 +28,292 @@ export type ParsedMessage = {
body: string;
};
enum MessageStatus {
export type OpticsStatus = {
status: MessageStatus;
events: AnnotatedLifecycleEvent[];
};
export enum MessageStatus {
Dispatched = 0,
Included = 1,
Relayed = 2,
Processed = 3,
}
export enum ReplicaMessageStatus {
None = 0,
Proven,
Processed,
}
export type EventCache = {
homeUpdate?: AnnotatedUpdate;
replicaUpdate?: AnnotatedUpdate;
process?: AnnotatedProcess;
};
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 receipt: ContractReceipt;
readonly event: DispatchEvent;
readonly messageHash: string;
readonly leafIndex: BigNumber;
readonly destinationAndNonce: BigNumber;
readonly committedRoot: string;
readonly dispatch: AnnotatedDispatch;
readonly message: ParsedMessage;
readonly home: core.Home;
readonly replica: core.Replica;
protected context: OpticsContext;
readonly context: OpticsContext;
protected cache: EventCache;
constructor(receipt: ContractReceipt, context: OpticsContext) {
this.receipt = receipt;
constructor(context: OpticsContext, dispatch: AnnotatedDispatch) {
this.context = context;
this.message = parseMessage(dispatch.event.args.message);
this.dispatch = dispatch;
this.home = context.mustGetCore(this.message.from).home;
this.replica = context.mustGetReplicaFor(
this.message.from,
this.message.destination,
);
this.cache = {};
}
// find the first dispatch log by attempting to parse them
let event;
const iface = new core.Home__factory().interface;
for (const log of receipt.logs) {
let parsed: LogDescription;
get receipt(): TransactionReceipt {
return this.dispatch.receipt;
}
static fromReceipt(
context: OpticsContext,
nameOrDomain: string | number,
receipt: TransactionReceipt,
): OpticsMessage[] {
const messages: OpticsMessage[] = [];
const home = new core.Home__factory().interface;
for (let log of receipt.logs) {
try {
parsed = iface.parseLog(log);
} catch (e) {
continue;
}
if (parsed.name === 'Dispatch') {
event = parsed as unknown as DispatchEvent;
}
const parsed = home.parseLog(log);
if (parsed.name === 'Dispatch') {
const dispatch = parsed as any;
dispatch.getBlock = () => {
return context
.mustGetProvider(nameOrDomain)
.getBlock(log.blockHash);
};
dispatch.getTransaction = () => {
return context
.mustGetProvider(nameOrDomain)
.getTransaction(log.transactionHash);
};
dispatch.getTransactionReceipt = () => {
return context
.mustGetProvider(nameOrDomain)
.getTransactionReceipt(log.transactionHash);
};
const annotated = annotate(
context.resolveDomain(nameOrDomain),
receipt,
dispatch as DispatchEvent,
);
annotated.name = 'Dispatch';
annotated.event.blockNumber = annotated.receipt.blockNumber;
const message = new OpticsMessage(context, annotated);
messages.push(message);
}
} catch (e) {}
}
return messages;
}
if (!event) {
throw new Error('No matching event found');
static singleFromReceipt(
context: OpticsContext,
nameOrDomain: string | number,
receipt: TransactionReceipt,
): OpticsMessage {
const messages: OpticsMessage[] = OpticsMessage.fromReceipt(
context,
nameOrDomain,
receipt,
);
if (messages.length !== 1) {
throw new Error('Expected single Dispatch in transaction');
}
return messages[0];
}
this.event = event;
static async fromTransactionHash(
context: OpticsContext,
nameOrDomain: string | number,
transactionHash: string,
): Promise<OpticsMessage[]> {
const provider = context.mustGetProvider(nameOrDomain);
const receipt = await provider.getTransactionReceipt(transactionHash);
if (!receipt) {
throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`);
}
return OpticsMessage.fromReceipt(context, nameOrDomain, receipt);
}
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);
static async singleFromTransactionHash(
context: OpticsContext,
nameOrDomain: string | number,
transactionHash: string,
): Promise<OpticsMessage> {
const provider = context.mustGetProvider(nameOrDomain);
const receipt = await provider.getTransactionReceipt(transactionHash);
if (!receipt) {
throw new Error(`No receipt for ${transactionHash} on ${nameOrDomain}`);
}
return OpticsMessage.singleFromReceipt(context, nameOrDomain, receipt);
}
this.context = context;
async getHomeUpdate(): Promise<AnnotatedUpdate | undefined> {
// if we have already gotten the event,
// return it without re-querying
if (this.cache.homeUpdate) {
return this.cache.homeUpdate;
}
// if not, attempt to query the event
const updateFilter = this.home.filters.Update(
undefined,
this.committedRoot,
);
const updateLogs: AnnotatedUpdate[] = await queryAnnotatedEvents<
UpdateTypes,
UpdateArgs
>(this.context, this.origin, this.home, updateFilter);
if (updateLogs.length === 1) {
// if event is returned, store it to the object
this.cache.homeUpdate = updateLogs[0];
} else if (updateLogs.length > 1) {
throw new Error('multiple home updates for same root');
}
// return the event or undefined if it doesn't exist
return this.cache.homeUpdate;
}
async getReplicaUpdate(): Promise<AnnotatedUpdate | undefined> {
// if we have already gotten the event,
// return it without re-querying
if (this.cache.replicaUpdate) {
return this.cache.replicaUpdate;
}
// if not, attempt to query the event
const updateFilter = this.replica.filters.Update(
undefined,
this.committedRoot,
);
const updateLogs: AnnotatedUpdate[] = await queryAnnotatedEvents<
UpdateTypes,
UpdateArgs
>(this.context, this.destination, this.replica, updateFilter);
if (updateLogs.length === 1) {
// if event is returned, store it to the object
this.cache.replicaUpdate = updateLogs[0];
} else if (updateLogs.length > 1) {
throw new Error('multiple replica updates for same root');
}
// return the event or undefined if it wasn't found
return this.cache.replicaUpdate;
}
async getProcess(): Promise<AnnotatedProcess | undefined> {
// if we have already gotten the event,
// return it without re-querying
if (this.cache.process) {
return this.cache.process;
}
// if not, attempt to query the event
const processFilter = this.replica.filters.Process(this.messageHash);
const processLogs = await queryAnnotatedEvents<ProcessTypes, ProcessArgs>(
this.context,
this.destination,
this.replica,
processFilter,
);
if (processLogs.length === 1) {
// if event is returned, store it to the object
this.cache.process = processLogs[0];
} else if (processLogs.length > 1) {
throw new Error('multiple replica updates for same root');
}
// return the update or undefined if it doesn't exist
return this.cache.process;
}
async events(): Promise<OpticsStatus> {
const events: AnnotatedLifecycleEvent[] = [this.dispatch];
// attempt to get Home update
const homeUpdate = await this.getHomeUpdate();
if (!homeUpdate) {
return {
status: MessageStatus.Dispatched, // the message has been sent; nothing more
events,
};
}
events.push(homeUpdate);
// attempt to get Replica update
const replicaUpdate = await this.getReplicaUpdate();
if (!replicaUpdate) {
return {
status: MessageStatus.Included, // the message was sent, then included in an Update on Home
events,
};
}
events.push(replicaUpdate);
// attempt to get Replica process
const process = await this.getProcess();
if (!process) {
// NOTE: when this is the status, you may way to
// query confirmAt() to check if challenge period
// on the Replica has elapsed or not
return {
status: MessageStatus.Relayed, // the message was sent, included in an Update, then relayed to the Replica
events,
};
}
events.push(process);
return {
status: MessageStatus.Processed, // the message was processed
events,
};
}
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}`,
);
// Note: return the timestamp after which it is possible to process messages within an Update
// the timestamp is most relevant during the time AFTER the Update has been Relayed to the Replica
// and BEFORE the message in question has been Processed.
// ****
// - the timestamp will be 0 if the Update has not been relayed to the Replica
// - after the Update has been relayed to the Replica, the timestamp will be non-zero forever (even after all messages in the Update have been processed)
// - if the timestamp is in the future, the challenge period has not elapsed yet; messages in the Update cannot be processed yet
// - if the timestamp is in the past, this does not necessarily mean that all messages in the Update have been processed
async confirmAt(): Promise<BigNumber> {
const update = await this.getHomeUpdate();
if (!update) {
return BigNumber.from(0);
}
const { newRoot } = update.event.args;
return this.replica.confirmAt(newRoot);
}
return await replica.messages(this.messageHash);
async replicaStatus(): Promise<ReplicaMessageStatus> {
return this.replica.messages(this.messageHash);
}
/// Returns true when the message is delivered
async delivered(): Promise<boolean> {
const status = await this.status();
return status === MessageStatus.Processed;
const status = await this.replicaStatus();
return status === ReplicaMessageStatus.Processed;
}
/// Resolves when the message has been delivered.
@ -148,6 +357,22 @@ export class OpticsMessage {
}
get transactionHash(): string {
return this.receipt.transactionHash;
return this.dispatch.event.transactionHash;
}
get messageHash(): string {
return this.dispatch.event.args.messageHash;
}
get leafIndex(): BigNumber {
return this.dispatch.event.args.leafIndex;
}
get destinationAndNonce(): BigNumber {
return this.dispatch.event.args.destinationAndNonce;
}
get committedRoot(): string {
return this.dispatch.event.args.committedRoot;
}
}

@ -13,4 +13,4 @@ const Kovan: TokenIdentifier = {
export default {
Alfajores,
Kovan,
};
};

@ -15,11 +15,7 @@ export class MultiProvider {
}
registerDomain(domain: Domain) {
this.domains.set(domain.domain, domain);
}
getDomain(domain: number): Domain | undefined {
return this.domains.get(domain);
this.domains.set(domain.id, domain);
}
get domainNumbers(): number[] {
@ -28,21 +24,38 @@ export class MultiProvider {
resolveDomain(nameOrDomain: string | number): number {
if (typeof nameOrDomain === 'string') {
return Array.from(this.domains.values()).filter(
const domains = Array.from(this.domains.values()).filter(
(domain) => domain.name === nameOrDomain,
)[0].domain;
);
if (domains.length === 0) {
throw new Error(`Domain not found: ${nameOrDomain}`);
}
return domains[0].id;
} else {
return nameOrDomain;
}
}
registerProvider(nameOrDomain: string | number, provider: Provider) {
const domain = this.resolveDomain(nameOrDomain);
getDomain(nameOrDomain: number | string): Domain | undefined {
return this.domains.get(this.resolveDomain(nameOrDomain));
}
if (!this.domains.get(domain)) {
throw new Error('Must have domain to register provider');
mustGetDomain(nameOrDomain: number | string): Domain {
const domain = this.getDomain(nameOrDomain);
if (!domain) {
throw new Error(`Domain not found: ${nameOrDomain}`);
}
return domain;
}
resolveDomainName(nameOrDomain: number | string): string | undefined {
return this.getDomain(nameOrDomain)?.name;
}
registerProvider(nameOrDomain: string | number, provider: Provider) {
const domain = this.mustGetDomain(nameOrDomain).id;
this.providers.set(domain, provider);
const signer = this.signers.get(domain);
if (signer) {
@ -63,6 +76,14 @@ export class MultiProvider {
return this.providers.get(domain);
}
mustGetProvider(nameOrDomain: string | number): Provider {
const provider = this.getProvider(nameOrDomain);
if (!provider) {
throw new Error('unregistered name or domain');
}
return provider;
}
registerSigner(nameOrDomain: string | number, signer: ethers.Signer) {
const domain = this.resolveDomain(nameOrDomain);
@ -103,7 +124,6 @@ export class MultiProvider {
async getAddress(nameOrDomain: string | number): Promise<string | undefined> {
const signer = this.getSigner(nameOrDomain);
return await signer?.getAddress();
}
}

@ -11,6 +11,7 @@
"./src/optics/contracts/*.ts",
"./src/optics/domains/*.ts",
"./src/optics/messages/*.ts",
"./src/optics/tokens/*.ts"
"./src/optics/tokens/*.ts",
"src/optics/events/*.ts"
]
}

File diff suppressed because one or more lines are too long

@ -17,7 +17,7 @@
"preserveWatchOutput": true,
"pretty": false,
"sourceMap": true,
"target": "es5",
"target": "es6",
"strict": true
}
}

Loading…
Cancel
Save