From a7f5f9d8f5e66e8cc9ea06be67db256dacf8927f Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:27:12 +0200 Subject: [PATCH 01/20] Updated exports in package.json. --- package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e068943..d73bb89 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,17 @@ "./api/util": "./api/util/index.js", "./ledger": "./ledger/index.js", "./ledger/assets": "./ledger/assets/index.js", - "./ledger/backend/contracts": "./ledger/backend/contracts/index.js", + "./ledger/backend/ethereum": "./ledger/backend/ethereum/index.js", "./test": "./test/index.js", "./test/assets": "./test/assets/index.js", "./test/ledger": "./test/ledger/index.js", "./utils": "./utils/index.js", "./utils/arrays": "./utils/arrays/index.js", - "./export/typedjson": "./export/typedjson.js" + "./utils/hexbytes": "./utils/hexbytes.js", + "./export/typedjson": "./export/typedjson.js", + "./crypto": "./crypto/index.js", + "./crypto/ethereum": "./crypto/ethereum/index.js", + "./crypto/substrate": "./crypto/substrate/index.js" }, "imports": { "#erdstall/*": "./*/index.js" From 397f99ee058937a9bd1d1bd1e775b90b3f53d028 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:50:13 +0200 Subject: [PATCH 02/20] Fixed FullExit. --- src/api/transactions/exitrequest.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/transactions/exitrequest.ts b/src/api/transactions/exitrequest.ts index e12ac16..fcf1070 100644 --- a/src/api/transactions/exitrequest.ts +++ b/src/api/transactions/exitrequest.ts @@ -30,9 +30,10 @@ const fullExitTypeName = "full"; @jsonObject export class FullExit extends ExitMode { - chain: number; + @jsonMember(Number) + chain?: number; - constructor(chain: number, immediate: boolean) { + constructor(chain: number | undefined, immediate: boolean) { super(immediate); this.chain = chain; } From 884563cc1800859317f0ff193f47cf83b0f66723 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:37:10 +0200 Subject: [PATCH 03/20] Reworked exit() and leave(). Currently simply performs a full exit, either to each native chain, or to an optionally specified chain ID. Later, we need to make this more customisable, but for now, it is sufficient. --- src/erdstall.ts | 3 ++- src/session.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/erdstall.ts b/src/erdstall.ts index c3d2d73..5fc543e 100644 --- a/src/erdstall.ts +++ b/src/erdstall.ts @@ -243,7 +243,7 @@ export interface Exiter { * to use `Leaver.leave` over the individual `Exiter.exit` call, because the * latter has to be manually followed by a call to `Withdrawer.withdraw`. */ - exit(): Promise; + exit(chain?: number): Promise; } /** @@ -261,6 +261,7 @@ export interface Leaver extends Exiter { * more information about theh return type. */ leave( + chain?: number, notify?: (message: string, stage: number, maxStages: number) => void, ): Promise< Map> >; } diff --git a/src/session.ts b/src/session.ts index 98f188e..364d7f3 100644 --- a/src/session.ts +++ b/src/session.ts @@ -12,6 +12,7 @@ import { Transfer, Mint, ExitRequest, + FullExit, TradeOffer, Trade, Burn, @@ -208,7 +209,7 @@ export class Session return { receipt, accepted }; } - async exit(): Promise { + async exit(chain?: number): Promise { if (!this.initialized) { return Promise.reject(ErrUnitialisedClient); } @@ -217,12 +218,14 @@ export class Session this.address, await this.nextNonce(), true, + new FullExit(chain, false) ); await exittx.sign(this.l2signer); return this.enclaveWriter.exit(exittx); } async leave( + chain?: number, notify?: (message: string, stage: number, maxStages: number) => void, ): Promise< Map> > { let skipped = 0; @@ -243,7 +246,7 @@ export class Session this.on_internal("phaseshift", cb); }); notify?.("awaiting exit proof", atStage++, maxStages); - const exitProof = (await this.exit()).accounts.get((this.address).key)!; + const exitProof = (await this.exit(chain)).accounts.get((this.address).key)!; notify?.("awaiting epoch sealing", atStage++, maxStages); await p.then(() => this.off_internal("phaseshift", cb)); @@ -253,8 +256,9 @@ export class Session const transactions = new Map>(); for(const [address, chains] of exitProof.proofs.entries()) { - for(const [chain, proofs] of chains.entries()) + for(let [chain, proofs] of chains.entries()) { + chain = Number(chain); transactions.set(chain, await this.withdraw( chain, exitProof.epoch, From b0cc7383dd804a15d4ceccc01b6497712210e1d1 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:37:57 +0200 Subject: [PATCH 04/20] Added ledger.getChainName(). --- src/ledger/chain.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/ledger/chain.ts b/src/ledger/chain.ts index 436774b..3f5806b 100644 --- a/src/ledger/chain.ts +++ b/src/ledger/chain.ts @@ -26,3 +26,21 @@ export enum Chain { Ropsten = 0xffff - 1, Rinkeby = 0xffff - 0, } + +export function getChainName(chain: Chain): string { + switch(chain) { + case Chain.Erdstall: return "Erdstall"; + case Chain.EthereumMainnet: return "Ethereum"; + case Chain.Ajuna: return "Ajuna"; + case Chain.Bajun: return "Bajun"; + case Chain.Goerli: return "Goerli"; + case Chain.Ropsten: return "Ropsten"; + case Chain.Rinkeby: return "Rinkeby"; + default:{ + if(chain >= Chain.TestChain0) + return `TestChain${chain - Chain.TestChain0}`; + else + return `Chain${chain}`; + } + } +} \ No newline at end of file From fae9b80acf33c5da35f20b857874a746928f3190 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:49:52 +0200 Subject: [PATCH 05/20] Updated chain configs, added clone(). Also made addresses cloneable. --- src/api/responses/clientconfig.ts | 40 ++++++++++++++------- src/crypto/address.ts | 4 ++- src/ledger/backend/ethereum/chainconfig.ts | 26 ++++++++++++-- src/ledger/backend/substrate/chainconfig.ts | 10 +++++- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/api/responses/clientconfig.ts b/src/api/responses/clientconfig.ts index 2030803..4df0611 100644 --- a/src/api/responses/clientconfig.ts +++ b/src/api/responses/clientconfig.ts @@ -4,7 +4,10 @@ import { ErdstallObject, registerErdstallType } from "#erdstall/api"; import { jsonObject } from "#erdstall/export/typedjson"; import { Backend, BackendChainConfig } from "#erdstall/ledger/backend"; +import { EthereumChainConfig } from "#erdstall/ledger/backend/ethereum/chainconfig"; +import { SubstrateChainConfig } from "#erdstall/ledger/backend/substrate/chainconfig"; import { Address, Crypto } from "#erdstall/crypto"; +import { EthereumAddress } from "#erdstall/crypto/ethereum"; import { Chain } from "#erdstall/ledger"; import { customJSON } from "../util"; @@ -19,21 +22,25 @@ export class ChainConfig { this.data = data; } + clone(): ChainConfig { + return new ChainConfig(this.id, this.type, this.data!.clone()); + } + static fromJSON(data: any): ChainConfig { - const d = JSON.parse(data); - switch (d.type) { + switch (data.type) { case "substrate": - return new ChainConfig(data.id, "substrate", { - blockStreamLAddr: d.data.blockStreamLAddr, - }); + return new ChainConfig(data.id, "substrate", + new SubstrateChainConfig(data.data.blockStreamLAddr)); case "ethereum": - return new ChainConfig(d.id, "ethereum", { - contract: d.data.contract, - networkID: d.data.networkID, - powDepth: d.data.powDepth, - }); + return new ChainConfig(data.id, "ethereum", + new EthereumChainConfig({ + contract: EthereumAddress.fromJSON(data.data.contract), + nodeRPC: data.data.nodeRPC, + networkID: data.data.networkID, + powDepth: data.data.powDepth, + })); default: - throw new Error(`unknown backend type: ${d.type}`); + throw new Error(`unknown backend type: ${data.type}`); } } @@ -87,7 +94,7 @@ export class ClientConfig extends ErdstallObject { static fromJSON(data: any): ClientConfig { let chains: ChainConfig[] = []; for (const conf of data.chains as ChainConfig[]) { - chains.push(ChainConfig.fromJSON(JSON.stringify(conf))); + chains.push(ChainConfig.fromJSON(conf)); } let enc: Map> = new Map(); for(const key in data.enclave ?? {}) { @@ -113,6 +120,15 @@ export class ClientConfig extends ErdstallObject { protected objectTypeName(): string { return clientConfigTypeName; } + + clone(): ClientConfig { + return new ClientConfig( + this.chains.map(c => c.clone()), + new Map>( + Array.from(this.enclave.entries()).map(([c,addr]) => [c, addr.clone()])), + this.enclaveNativeSigner.clone() + ); + } } registerErdstallType(clientConfigTypeName, ClientConfig); diff --git a/src/crypto/address.ts b/src/crypto/address.ts index 7db9e4f..5e4b43b 100644 --- a/src/crypto/address.ts +++ b/src/crypto/address.ts @@ -16,12 +16,14 @@ export function registerAddressType( export abstract class Address<_C extends Crypto> { abstract type(): _C; - get key(): string { return Address.toJSON(this); } + get key(): string { return JSON.stringify(Address.toJSON(this)); } abstract equals(other: Address<_C>): boolean; abstract toString(): string; abstract toJSON(): string; abstract get keyBytes(): Uint8Array; + clone(): Address<_C> { return Address.fromJSON(Address.toJSON(this)) as Address<_C>; } + static ensure(addr: string | Address): Address { if (addr === undefined) return addr; if (addr instanceof Address) return addr; diff --git a/src/ledger/backend/ethereum/chainconfig.ts b/src/ledger/backend/ethereum/chainconfig.ts index c5831c9..83377bf 100644 --- a/src/ledger/backend/ethereum/chainconfig.ts +++ b/src/ledger/backend/ethereum/chainconfig.ts @@ -3,8 +3,30 @@ import { EthereumAddress } from "#erdstall/crypto/ethereum"; -export interface EthereumChainConfig { +export class EthereumChainConfig { contract: EthereumAddress; - networkID: string; + networkID?: string; + nodeRPC?: string; powDepth: number; + + constructor(arg: { + contract: EthereumAddress, + networkID?: string, + nodeRPC?: string, + powDepth: number + }) { + this.contract = arg.contract; + this.networkID = arg.networkID; + this.nodeRPC = arg.nodeRPC; + this.powDepth = arg.powDepth; + } + + clone(): EthereumChainConfig { + return new EthereumChainConfig({ + contract: this.contract.clone() as EthereumAddress, + networkID: this.networkID, + nodeRPC: this.nodeRPC, + powDepth: this.powDepth + }); + } } diff --git a/src/ledger/backend/substrate/chainconfig.ts b/src/ledger/backend/substrate/chainconfig.ts index 3a270af..385d670 100644 --- a/src/ledger/backend/substrate/chainconfig.ts +++ b/src/ledger/backend/substrate/chainconfig.ts @@ -1,6 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 "use strict"; -export interface SubstrateChainConfig { +export class SubstrateChainConfig { blockStreamLAddr: string; + + constructor(blockStreamLAddr: string) { + this.blockStreamLAddr = blockStreamLAddr; + } + + clone() { + return new SubstrateChainConfig(this.blockStreamLAddr); + } } From 8435da5f3e7a37a0cef0a0889f844ce46347d064 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 14:53:05 +0200 Subject: [PATCH 06/20] Made updateNonce() concurrency-safe. --- src/session.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/session.ts b/src/session.ts index 364d7f3..3886624 100644 --- a/src/session.ts +++ b/src/session.ts @@ -88,6 +88,7 @@ export class Session // Filled dynamically when we receive configs. protected clients: Map>; private blockchainWriteCtors: BackendSessionConstructors; + private updatingNonce?: Promise; static async create( l2signer: crypto.Signer, @@ -145,15 +146,22 @@ export class Session return this.nonce++; } - // Fetches the current nonce from the enclave. Only overwrites the nonce if - // it has an invalid value, so this function can be called concurrently. - private async updateNonce(): Promise { + private async updateNonceInternal(): Promise { const acc = await this.enclaveWriter.getAccount(this.address); if (!this.nonce) { this.nonce = acc.account.nonce + 1n; } } + // Fetches the current nonce from the enclave. Only overwrites the nonce if + // it has an invalid value, so this function can be called concurrently. + async updateNonce(): Promise { + if(this.updatingNonce) return this.updatingNonce; + this.updatingNonce = this.updateNonceInternal(); + await this.updatingNonce; + this.updatingNonce = undefined; + } + async onboard(): Promise { return this.enclaveWriter.onboard(this.address); } From ed4ddf0dc87c93ab0bb7862d90ca90c98fa2720a Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:14:47 +0200 Subject: [PATCH 07/20] Added AssetType, AssetID.toString(). --- src/crypto/asset.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/crypto/asset.ts b/src/crypto/asset.ts index 32366a4..fe49b66 100644 --- a/src/crypto/asset.ts +++ b/src/crypto/asset.ts @@ -4,8 +4,23 @@ import { Chain } from "#erdstall/ledger/chain"; import { Address, Crypto } from "#erdstall/crypto"; import { ethers } from "ethers"; +import { toHex } from "#erdstall/utils/hexbytes"; +export enum AssetType { + Fungible, + NFT +} + +export function AssetTypeName(t: AssetType): string { + switch(t) + { + case AssetType.Fungible: return "FUN"; + case AssetType.NFT: return "NFT"; + } + throw new Error(`Unknown AssetType: ${t}`); +} + export class AssetID { // [Origin Chain][AssetType][ID LocalAsset] packed into fixed-size array. bytes: Uint8Array; @@ -25,7 +40,7 @@ export class AssetID { static fromMetadata( chain: Chain, - type: number, + type: AssetType, localID: Uint8Array, ): AssetID { const bytes = new Uint8Array(3 + localID.length); @@ -41,8 +56,8 @@ export class AssetID { return origin as Chain; } - type(): number { - return this.bytes[2]; + type(): AssetType { + return this.bytes[2] as AssetType; } localID(): Uint8Array { @@ -59,4 +74,12 @@ export class AssetID { } return 0; } + + toString(): string { + return `${ + this.origin() + }/${ + AssetTypeName(this.type()) + }/${toHex(this.localID(), "")}`; + } } \ No newline at end of file From 35ef411763b69f66b977b5cd3fd0f7d2f1b0d26c Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:19:01 +0200 Subject: [PATCH 08/20] Improved assets. --- src/ledger/assets/amount.ts | 5 +++++ src/ledger/assets/asset.ts | 3 +++ src/ledger/assets/assets.ts | 14 +++++++++++--- src/ledger/assets/tokens.ts | 5 +++++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/ledger/assets/amount.ts b/src/ledger/assets/amount.ts index 3dbead1..d9687b4 100644 --- a/src/ledger/assets/amount.ts +++ b/src/ledger/assets/amount.ts @@ -11,6 +11,7 @@ import { assertUint256, registerAssetType, } from "./asset"; +import { AssetType } from "#erdstall/crypto"; import { bigTo0xEven } from "#erdstall/export/typedjson"; /** Amount represents a currency amount in its smallest unit. */ @@ -23,6 +24,8 @@ export class Amount extends Asset { this.value = v; } + assetType(): AssetType.Fungible { return AssetType.Fungible; } + toJSON() { return bigTo0xEven(this.value); } @@ -31,6 +34,8 @@ export class Amount extends Asset { return new Amount(BigInt(hexString)); } + toString() { return this.value.toString(); } + typeTag(): TypeTagName { return TypeTags.Amount; } diff --git a/src/ledger/assets/asset.ts b/src/ledger/assets/asset.ts index cac47e2..69a71fb 100644 --- a/src/ledger/assets/asset.ts +++ b/src/ledger/assets/asset.ts @@ -2,6 +2,7 @@ "use strict"; import { isUint256 } from "#erdstall/api/util"; +import { AssetType } from "#erdstall/crypto"; export const TypeTags = { Amount: "uint", @@ -30,6 +31,8 @@ export function registerAssetType( export abstract class Asset { abstract toJSON(): any; + abstract assetType(): AssetType; + static fromJSON(json: any): Asset { for (const key in json) { if (assetImpls.has(key)) { diff --git a/src/ledger/assets/assets.ts b/src/ledger/assets/assets.ts index be5837d..d40766c 100644 --- a/src/ledger/assets/assets.ts +++ b/src/ledger/assets/assets.ts @@ -15,6 +15,7 @@ import { Backend } from "#erdstall/ledger/backend"; import { Amount } from "./amount"; import { Tokens } from "./tokens"; import { Chain } from "../chain"; +import { toHex, parseHex } from "#erdstall/utils/hexbytes"; export const ETHZERO = "0x0000000000000000000000000000000000000000"; @@ -108,7 +109,7 @@ export class LocalFungibles { } addAsset(localID: Uint8Array, asset: Amount) { - const token = ethers.hexlify(localID); + const token = toHex(localID, "0x"); const a = this.assets.get(token); if (a !== undefined) { a.add(asset); @@ -194,10 +195,17 @@ export class LocalAsset { public id: Uint8Array; constructor(id: Uint8Array) { this.id = id; + if(this.id.length != 32) + throw new Error(`Invalid length (${this.id.length}/32)`); + } + + get isZero() { + return Array.from(this.id).every(byte => byte === 0); } - get key(): string { - return this.id.toString(); + get key() { return toHex(this.id, ""); } + static fromKey(key: string): LocalAsset { + return new LocalAsset(parseHex(key)); } } diff --git a/src/ledger/assets/tokens.ts b/src/ledger/assets/tokens.ts index 4c9525b..2fbd9ab 100644 --- a/src/ledger/assets/tokens.ts +++ b/src/ledger/assets/tokens.ts @@ -12,6 +12,7 @@ import { } from "./asset"; import { Amount } from "./amount"; import { bigTo0xEven } from "#erdstall/export/typedjson"; +import { AssetType } from "#erdstall/crypto"; export const ErrIDAlreadyContained = new Error( "given ID already contained in tokens", @@ -20,6 +21,8 @@ export const ErrIDAlreadyContained = new Error( export class Tokens extends Asset { public value: bigint[]; + assetType(): AssetType.NFT { return AssetType.NFT; } + constructor(v: bigint[]) { super(); this.value = v.sort((a, b) => (a < b ? -1 : a == b ? 0 : 1)); @@ -48,6 +51,8 @@ export class Tokens extends Asset { return new Tokens(s); } + toString() { return "[" + this.value.join(", ") + "]"; } + typeTag(): TypeTagName { return TypeTags.Tokens; } From 766b37b21c07810af2181fb1913aab3e9fa5c423 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:02:58 +0200 Subject: [PATCH 09/20] EthereumTokenProvider: improved fetchHolders(). --- src/ledger/backend/ethereum/tokencache.ts | 30 ++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/ledger/backend/ethereum/tokencache.ts b/src/ledger/backend/ethereum/tokencache.ts index 1ce2280..fc2c7ab 100644 --- a/src/ledger/backend/ethereum/tokencache.ts +++ b/src/ledger/backend/ethereum/tokencache.ts @@ -39,25 +39,27 @@ export class EthereumTokenProvider { } async fetch_holders( - erdstallAddr: EthereumAddress, contract: Erdstall, - ): Promise> { + ): Promise { if(!this.set_holders) - return this.holders; + { + await this.holders; + return; + } - const holders = new Map(); - holders.set("ERC20", EthereumAddress.fromString( - await contract.tokenHolders(0))); - holders.set("ERC721", EthereumAddress.fromString( - await contract.tokenHolders(1))); - holders.set("ETH", EthereumAddress.fromString( - await contract.tokenHolders(2))); - - this.resolve_holders(holders); - return holders; + try { + const holders = new Map(); + holders.set("ERC20", EthereumAddress.fromString( + await contract.tokenHolders(0))); + holders.set("ERC721", EthereumAddress.fromString( + await contract.tokenHolders(1))); + holders.set("ETH", EthereumAddress.fromString( + await contract.tokenHolders(2))); + this.resolve_holders(holders); + } catch(e) { this.set_holders = undefined; this.fail_holders!(e); } } - // query token holder for a token type. Fails if none is configured within a reasonable timeout. + // query token holder for a token type. Fails if none is configured within a reasonable timeout. Remember to call fetch_holders()! async tokenHolderFor( ttype: TokenType, ): Promise { From 571dec04c7e0ace7024681c2f91ba30ed99119f3 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:05:06 +0200 Subject: [PATCH 10/20] EthereumTokenProvider: added wrapped token query. Also added cache for their results. Users can now query wrapped tokens for an Erdstall multi-chain token identifier. --- src/ledger/backend/ethereum/tokencache.ts | 43 ++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ledger/backend/ethereum/tokencache.ts b/src/ledger/backend/ethereum/tokencache.ts index fc2c7ab..abd5094 100644 --- a/src/ledger/backend/ethereum/tokencache.ts +++ b/src/ledger/backend/ethereum/tokencache.ts @@ -2,10 +2,11 @@ "use strict"; import { ethers } from "ethers"; -import { Address, addressKey } from "#erdstall/crypto"; +import { Address, AssetID, AssetType } from "#erdstall/crypto"; import { EthereumAddress } from "#erdstall/crypto/ethereum"; import { TokenType } from "./tokentype"; import { Chain } from "#erdstall/ledger"; +import { LocalAsset } from "#erdstall/ledger/assets"; import { Erdstall, ERC20__factory, ERC20Holder, ERC20Holder__factory, @@ -18,6 +19,8 @@ import { export class EthereumTokenProvider { readonly holders: Promise>; readonly chain: Chain; + // NOTE: undeployed tokens always miss the cache and cause a request. + private cache = new Map(); private set_holders?: (arg: Map) => void; private fail_holders?: (arg: any) => void; @@ -96,4 +99,42 @@ export class EthereumTokenProvider { const holder = await this.tokenHolderFor("ETH"); return ETHHolder__factory.connect(holder.toString(), provider); } + + async getWrappedFungible(provider: ethers.Provider, origin: Chain, local: LocalAsset): Promise { + if(origin === this.chain) { + throw new Error("not a wrapped token…"); + } + const asset = AssetID.fromMetadata(origin, AssetType.Fungible, local.id); + const key = asset.toString(); + let addr: EthereumAddress | undefined; + if(addr = this.cache.get(key)) + return addr!.clone() as EthereumAddress; + + const holder = await this.getERC20Holder(provider); + addr = EthereumAddress.fromString( + await holder.deployedToken(origin, local.id)); + if(addr.isZero()) + return undefined; + this.cache.set(key, addr.clone() as EthereumAddress) + return addr!; + } + + async getWrappedNFT(provider: ethers.Provider, origin: Chain, local: LocalAsset): Promise { + if(origin === this.chain) { + throw new Error("not a wrapped token…"); + } + const asset = AssetID.fromMetadata(origin, AssetType.NFT, local.id); + const key = asset.toString() + let addr: EthereumAddress | undefined; + if(addr = this.cache.get(key)) + return addr!.clone() as EthereumAddress; + + const holder = await this.getERC721Holder(provider); + addr = EthereumAddress.fromString( + await holder.deployedToken(origin, local.id)); + if(addr.isZero()) + return undefined; + this.cache.set(key, addr.clone() as EthereumAddress) + return addr; + } } From 69094a2b4f35c8a1ee0b440304249e4ba5945ffa Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:26:13 +0200 Subject: [PATCH 11/20] Improved Ethereum ReadConn token handling. --- src/ledger/backend/ethereum/readconn.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/ledger/backend/ethereum/readconn.ts b/src/ledger/backend/ethereum/readconn.ts index a92eaec..e987ea2 100644 --- a/src/ledger/backend/ethereum/readconn.ts +++ b/src/ledger/backend/ethereum/readconn.ts @@ -2,7 +2,7 @@ "use strict"; import { ErdstallEventHandler } from "#erdstall"; -import { AssetID, Address } from "#erdstall/crypto"; +import { AssetID, AssetType, Address } from "#erdstall/crypto"; import { EthereumAddress } from "#erdstall/crypto/ethereum"; import { LedgerEvent } from "#erdstall/ledger"; import { LocalAsset } from "#erdstall/ledger/assets"; @@ -31,6 +31,7 @@ export class LedgerReadConn implements LedgerReader<"ethereum"> { this.contract = contract; this.eventCache = new Map(); this.tokenCache = tokenCache; + this.tokenCache.fetch_holders(this.contract); } on( @@ -74,22 +75,20 @@ export class LedgerReadConn implements LedgerReader<"ethereum"> { // return Address.fromString(this.contract.address); } - async getWrappedToken(token: AssetID): Promise { + async getWrappedToken(token: AssetID): Promise { const provider = this.contract.runner!.provider!; switch(token.type()) { default: throw new Error(`unhandled token type ${token.type()}!`); - case 0: + case AssetType.Fungible: { - const holder = await this.tokenCache.getERC20Holder(provider); - return EthereumAddress.fromString( - await holder.deployedToken(token.origin(), token.localID())); + return await this.tokenCache.getWrappedFungible(provider, + token.origin(), new LocalAsset(token.localID())); } - case 1: + case AssetType.NFT: { - const holder = await this.tokenCache.getERC721Holder(provider); - return EthereumAddress.fromString( - await holder.deployedToken(token.origin(), token.localID())); + return await this.tokenCache.getWrappedNFT(provider, + token.origin(), new LocalAsset(token.localID())); } } } From 9213f20ef8a7449b58e913ee351572bcf6c292c0 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:57:00 +0200 Subject: [PATCH 12/20] Made Ethereum WriteConn multi-chain aware. --- src/ledger/backend/ethereum/writeconn.ts | 47 ++++++++++++++++++------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/ledger/backend/ethereum/writeconn.ts b/src/ledger/backend/ethereum/writeconn.ts index 60466e1..ea8739c 100644 --- a/src/ledger/backend/ethereum/writeconn.ts +++ b/src/ledger/backend/ethereum/writeconn.ts @@ -3,7 +3,8 @@ import { ethers, Signer as EthersSigner } from "ethers"; import * as common from "./contracts/common"; -import { Asset, ChainAssets, Amount } from "#erdstall/ledger/assets"; +import { Asset, ChainAssets, LocalAsset, Amount } from "#erdstall/ledger/assets"; +import { AssetID } from "#erdstall/crypto"; import { EthereumAddress as Address, EthereumSigner as Signer, @@ -15,9 +16,11 @@ import { LedgerReadConn } from "./readconn"; import { depositors, Calls } from "./tokenmanager"; import { EthereumTokenProvider } from "./tokencache"; import { TransactionGenerator } from "#erdstall/utils"; -import { Chain } from "#erdstall/ledger/chain"; +import { Chain, getChainName } from "#erdstall/ledger/chain"; import { encodePackedAssets } from "./ethwrapper"; +import { toHex } from "#erdstall/utils/hexbytes"; + type TransactionName = "approve" | "deposit" | "withdraw"; export class LedgerWriteConn @@ -25,14 +28,15 @@ export class LedgerWriteConn implements LedgerWriter<"ethereum"> { readonly signer: Signer; - readonly chain: number; + readonly chain: Chain; constructor( contract: Erdstall, - chain: number, - tokenCache: EthereumTokenProvider, + chain: Chain, + tokenCache?: EthereumTokenProvider, ) { - super(contract, tokenCache); + super(contract, tokenCache ?? new EthereumTokenProvider(chain)); + if(!(contract.runner as any)?.signMessage) throw new Error("LedgerWriteConn: expected a signer provider"); this.signer = new Signer(contract.runner! as EthersSigner); @@ -78,10 +82,29 @@ export class LedgerWriteConn // NOTE: We do not only support `Chain.EthereumMainnet`. // TODO: handle wrapped assets?. - const addStage = async (tokenAddr: string, amount: Asset) => { - const tokenAddrAddr = Address.fromString(tokenAddr); + const addStage = async (chain: Chain, tokenAddr: LocalAsset, amount: Asset) => { + + let tokenAddrAddr: Address; + if(chain == this.chain) + { + // Native tokens are the last 20 bytes of the LocalAsset + tokenAddrAddr = new Address(tokenAddr.id.slice(-20)); + if(!tokenAddr.id.slice(0,12).every(x => x == 0)) + throw new Error(`Invalid localID: ${toHex(tokenAddr.id)}`); + } + else + { + const assetID = AssetID.fromMetadata( + chain, + amount.assetType(), + tokenAddr.id); + tokenAddrAddr = await this.getWrappedToken(assetID) ?? (() => { + throw new Error(`Wrapped token for ${assetID} not yet deployed on ${getChainName(this.chain)}`); + })(); + } + const ttype = (amount instanceof Amount) - ? tokenAddrAddr.isZero() + ? tokenAddrAddr.isZero() && (chain === this.chain) ? "ETH" : "ERC20" : "ERC721"; @@ -96,12 +119,12 @@ export class LedgerWriteConn calls.push(...depositCalls); }; - for (const [_chain, asset] of assets.assets) { + for (const [chain, asset] of assets.assets) { for (const [tokenAddr, amount] of asset.fungibles.assets) { - await addStage(tokenAddr, amount); + await addStage(chain, LocalAsset.fromKey(tokenAddr), amount); } for (const [tokenAddr, nfts] of asset.nfts.assets) { - await addStage(tokenAddr, nfts); + await addStage(chain, LocalAsset.fromKey(tokenAddr), nfts); } } From 3d283047456112a7eb3f061c0864a9b2a5548083 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:29:24 +0200 Subject: [PATCH 13/20] Fixed EthereumSigner. No longer assumes that the internal ethers signer already has a blockchain connection. --- src/crypto/ethereum/signer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/crypto/ethereum/signer.ts b/src/crypto/ethereum/signer.ts index f93ef8f..b6a04d2 100644 --- a/src/crypto/ethereum/signer.ts +++ b/src/crypto/ethereum/signer.ts @@ -1,17 +1,21 @@ // SPDX-License-Identifier: Apache-2.0 "use strict"; -import { Signer as EthersSigner, ethers } from "ethers"; +import { Signer as EthersSigner, Provider, ethers } from "ethers"; import { Signer, Address, Signature } from "#erdstall/crypto"; import { EthereumSignature } from "./signature"; import { EthereumAddress } from "./address"; // Compile-time check that the EthersSigner implements the Signer interface. export class EthereumSigner implements Signer<"ethereum"> { - readonly ethersSigner: EthersSigner; + #ethersSigner: EthersSigner; + + get ethersSigner(): EthersSigner { return this.#ethersSigner; } + + connect(p: Provider) { this.#ethersSigner = this.#ethersSigner.connect(p); } constructor(ethersSigner: EthersSigner) { - this.ethersSigner = ethersSigner; + this.#ethersSigner = ethersSigner; } async sign(message: Uint8Array): Promise> { From 66de0ae43acff8ff2a37b80c0f6f17843c416b84 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:29:29 +0200 Subject: [PATCH 14/20] Added backend type() query to signers. --- src/crypto/ethereum/signer.ts | 2 ++ src/crypto/signer.ts | 1 + src/crypto/substrate/signer.ts | 3 +++ 3 files changed, 6 insertions(+) diff --git a/src/crypto/ethereum/signer.ts b/src/crypto/ethereum/signer.ts index b6a04d2..181ce05 100644 --- a/src/crypto/ethereum/signer.ts +++ b/src/crypto/ethereum/signer.ts @@ -18,6 +18,8 @@ export class EthereumSigner implements Signer<"ethereum"> { this.#ethersSigner = ethersSigner; } + type(): "ethereum" { return "ethereum"; } + async sign(message: Uint8Array): Promise> { const sig = await this.ethersSigner.signMessage( ethers.getBytes(ethers.keccak256(message))); diff --git a/src/crypto/signer.ts b/src/crypto/signer.ts index ec05c1a..ec95c0f 100644 --- a/src/crypto/signer.ts +++ b/src/crypto/signer.ts @@ -8,6 +8,7 @@ import { Crypto, Address, Signature } from "#erdstall/crypto"; const signatureImpls = new Map>>(); export abstract class Signer { + abstract type(): B; abstract address(): Promise>; abstract sign(msg: Uint8Array): Promise>; } diff --git a/src/crypto/substrate/signer.ts b/src/crypto/substrate/signer.ts index 7fb0e38..fb1d76f 100644 --- a/src/crypto/substrate/signer.ts +++ b/src/crypto/substrate/signer.ts @@ -18,6 +18,9 @@ export class SubstrateSigner implements Signer<"substrate"> { this.keyPair = keyPair; } + type(): "substrate" { return "substrate"; } + + async sign(message: Uint8Array): Promise> { await cryptoWaitReady(); const sig = sr25519Sign(message, this.keyPair); From dda394102e990ac81e3a368a3b7e7867b06ade56 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:10:09 +0200 Subject: [PATCH 15/20] Only connect to chain if we have a fitting signer. --- src/session.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 3886624..4f7dcef 100644 --- a/src/session.ts +++ b/src/session.ts @@ -327,10 +327,20 @@ export class Session continue; } + if(this.l2signer.type() !== chainCfg.type) + { + console.warn(`No compatible signer for ${ + chainCfg.type + } chain <${ + chainCfg.id + }>: not creating a backend client.`); + continue; + } + const ctor = this.blockchainWriteCtors[chainCfg.type]!; this.clients.set( chainCfg.id, - (ctor.initializer as unknown as any)(chainCfg.data)); + (ctor.initializer as unknown as any)(chainCfg, this.l2signer)); } // Forward all cached events to respective clients. From 7403b74f4fc0a4c9e75d8466566e3b986359c072 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:10:52 +0200 Subject: [PATCH 16/20] Tracking config in in Client and exposing it. --- src/client.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 7ba4b76..e8e74a2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -11,7 +11,7 @@ import { import * as crypto from "#erdstall/crypto"; import { AttestationResult, ClientConfig } from "#erdstall/api/responses"; import { Enclave, isEnclaveEvent, EnclaveReader } from "#erdstall/enclave"; -import { Account, isLedgerEvent, LedgerEvent } from "#erdstall/ledger"; +import { Chain, Account, isLedgerEvent, LedgerEvent } from "#erdstall/ledger"; import { LocalAsset } from "#erdstall/ledger/assets"; import { Backend } from "#erdstall/ledger/backend"; import { BackendChainConfig } from "#erdstall/ledger/backend/backends"; @@ -50,6 +50,15 @@ export class Client implements ErdstallClient { private blockchainReadCtors: BackendClientConstructors; + #config?: ClientConfig; + + get config(): ClientConfig | undefined { return this.#config?.clone(); } + get chainTypes() { + return new Map( + this.#config!.chains.map(cfg => [cfg.id, cfg.type])); + } + + constructor( enclaveConn: (EnclaveReader & InternalEnclaveWatcher) | URL, blockchainReadCtors: BackendClientConstructors, @@ -256,6 +265,8 @@ export class Client implements ErdstallClient { this.enclaveConn.once("error", reject); this.enclaveConn.once("config", (config: ClientConfig) => { + this.#config = config; + onConfigHandler(config); this.erdstallOneShotHandlerCache.clear(); From eead17916461a78632fbd52396fd972aa429721b Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:19:56 +0200 Subject: [PATCH 17/20] Fixed exports. --- src/api/util/index.ts | 1 + src/ledger/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/api/util/index.ts b/src/api/util/index.ts index e126dc8..7872caa 100644 --- a/src/api/util/index.ts +++ b/src/api/util/index.ts @@ -5,3 +5,4 @@ export * from "./bigint"; export { ABIEncoder, ABIPacked } from "./abiencoder"; export type { ABIEncodable, ABIValue } from "./abiencoder"; export * from "./customjson"; +export * from "./pending_transaction"; diff --git a/src/ledger/index.ts b/src/ledger/index.ts index 2dbd715..e4bd96f 100644 --- a/src/ledger/index.ts +++ b/src/ledger/index.ts @@ -4,5 +4,6 @@ export * from "./account"; export * from "./chain"; export * from "./event"; +export * from "./backend/backends"; export * from "./backend/writer"; export * from "./backend/reader"; From 8d1446d989bfa3d917a8fa8a8730adea133ec7a6 Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:24:22 +0200 Subject: [PATCH 18/20] Fixed reconnect logic. No longer reconnects upon manual closure by the user. --- src/enclave/connection.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/enclave/connection.ts b/src/enclave/connection.ts index 9aee904..b5a615e 100644 --- a/src/enclave/connection.ts +++ b/src/enclave/connection.ts @@ -117,6 +117,7 @@ export class Enclave implements EnclaveWriter { private calls: Map; private id: number; + private opened: boolean = false; private globallySubscribed: boolean; private individuallySubscribed: Set>; private phaseShiftSubscribed: boolean; @@ -380,16 +381,20 @@ export class Enclave implements EnclaveWriter { this.callEvent("error", new Error("connection error")); + if(this.opened) { + setTimeout(() => { + try { + this.connect(); + } catch {} + }, 1000); + } + this.provider.close(); - setTimeout(() => { - try { - this.connect(); - } catch {} - }, 1000); } private onOpen(_: Event) { const calls = []; + this.opened = true; if (this.globallySubscribed) calls.push(new SubscribeTXs(), new SubscribeBalanceProofs()); @@ -408,6 +413,7 @@ export class Enclave implements EnclaveWriter { } private onClose(_: Event) { + this.opened = false; this.callEvent("close", {} as any); } } From 1d06ed1db567cb3e97b7199055dec98afeb6464c Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 15:26:41 +0200 Subject: [PATCH 19/20] Improved error reporting. --- src/crypto/ethereum/address.ts | 2 ++ src/enclave/connection.ts | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/crypto/ethereum/address.ts b/src/crypto/ethereum/address.ts index ede7a2d..93d44c8 100644 --- a/src/crypto/ethereum/address.ts +++ b/src/crypto/ethereum/address.ts @@ -17,6 +17,8 @@ export class EthereumAddress extends Address<"ethereum"> implements ABIValue { private value: Uint8Array; constructor(value: Uint8Array) { super(); + if(value.length !== 20) + throw new Error(`Invalid length (${value.length}/20)`); this.value = value; } diff --git a/src/enclave/connection.ts b/src/enclave/connection.ts index b5a615e..e702967 100644 --- a/src/enclave/connection.ts +++ b/src/enclave/connection.ts @@ -318,12 +318,11 @@ export class Enclave implements EnclaveWriter { const [resolve, reject] = this.calls.get(msg.id)!; this.calls.delete(msg.id); if (msg.error) { - reject(msg.error); - this.callEvent("error", msg.error); + reject(new Error(msg.error)); + return this.callEvent("error", msg.error); } else { - resolve(msg.data); + return resolve(msg.data); } - return } else if(msg.error) { console.error("unexpected error:", msg.error); this.callEvent("error", msg.error); From 688f126dddb613932d27afb3677b53be9464bdcf Mon Sep 17 00:00:00 2001 From: Steffen Rattay Date: Thu, 10 Oct 2024 16:29:17 +0200 Subject: [PATCH 20/20] Fixed encoding tests. No idea how 32-byte ethereum addresses happened. The improved error reporting in the constructor caught it. --- src/api/transactions/transaction.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/api/transactions/transaction.spec.ts b/src/api/transactions/transaction.spec.ts index 59dc988..fc2062a 100644 --- a/src/api/transactions/transaction.spec.ts +++ b/src/api/transactions/transaction.spec.ts @@ -86,11 +86,8 @@ const testTrade = const testError = '{"id":"an-id","error":"could not get proof"}'; const testSubscribeTXs = - '{"id":"an-id","data":{"type":"SubscribeTXs","data":{"who":{"type":"ethereum","data":"0x165803a9e8ae54dd4e8da44a8d19897c19cb91aeaea6afc67a7e8c6f35323716"}, "cancel": false}}}'; + '{"id":"an-id","data":{"type":"SubscribeTXs","data":{"who":{"type":"ethereum","data":"0x8d19897c19cb91aeaea6afc67a7e8c6f35323716"}, "cancel": false}}}'; const testSubscribeBalanceProofs = - '{"id":"an-id","data":{"type":"SubscribeBalanceProofs","data":{"who":{"type":"ethereum","data":"0x165803a9e8ae54dd4e8da44a8d19897c19cb91aeaea6afc67a7e8c6f35323716"}}}}'; + '{"id":"an-id","data":{"type":"SubscribeBalanceProofs","data":{"who":{"type":"ethereum","data":"0x8d19897c19cb91aeaea6afc67a7e8c6f35323716"}}}}'; const testSubscribePhaseShifts = - '{"id":"an-id","data":{"type":"SubscribePhaseShifts","data":{"cancel": false}}}'; - -const testHashing = - '{"sig":{"type":"test","data":{"address":{"type":"test","data":"0x11342d7d8ae3c7ead6845ade6838864c64197ec6a3f9ea8b6e91d6e95cbd1b80"},"msg":"eyJ2YWx1ZSI6eyJkYXRhIjp7Im5vbmNlIjoiMjEyNTY5MzA0NDkxNjI0OTIyOSIsInJlY2lwaWVudCI6eyJkYXRhIjoiOWZkNGJlYjE5MzYzYWE4NTg2MzIzNTQ0NTA2MmJjMzc5NTU1NjFjNDMyOGIxMzhmYTc0ZDQ2NTljYzgxODdkMCIsInR5cGUiOiJ0ZXN0In0sInNlbmRlciI6eyJkYXRhIjoiMTEzNDJkN2Q4YWUzYzdlYWQ2ODQ1YWRlNjgzODg2NGM2NDE5N2VjNmEzZjllYThiNmU5MWQ2ZTk1Y2JkMWI4MCIsInR5cGUiOiJ0ZXN0In0sInZhbHVlcyI6eyIzMDgxMCI6eyJuZnRzIjp7IjljMzk0ZDE3YjJiNmI5ZGM1YjU3MTg5ODkxNmY0MGQ5Y2ViOTY0NmU1MWNmNDFhY2VjZTU0ODQzOTk1MDFhYTYiOlsiMHg2YTg5YmY0YzdhZGEzMjQ4YTUwNzYyMGU4NWJjMzBmMDQ3M2IyNmQ3MDdiZGUxZGViZDEwOWNlM2NhMWJkOTRkIiwiMHhmMTY2NWQ4MWNhYWI4MTNhMjlkOTFmNTIxMjhjOGQ4YjcwOWQ2MGI0MmUwNzhhOWFiODYxYjlhZDRkMzEyODA1IiwiMHhmZTc3MzRjM2RiMTlkM2Y5Y2Y2NWEyYjY4ZTVmMmIyYjljZGI5ODFlMjdkYmM1NGRkZWQ0YjRmNmU3ODA1NzE1Il19fSwiMzc2NzYiOnsiZnVuZ2libGVzIjp7IjdjOWRlMDM0ZmFiNmViOTU3ZDNmNzNmOWU5ZTE2OGE4OWE3ZTFkZGZiMzFjNTQyOWYxNDQxZDRkZDBmNGI5NDAiOiIweDk0OWMxODY3YzdmZGUwYTZmODFlMmY2MmNhYWNkOWExMWE1MjMyZmJmYWM5YmQzZmE5MGU5ZWIxOGY0Mzk2MyJ9fSwiNDY4MzIiOnsiZnVuZ2libGVzIjp7IjQzMTllOGYxOTZjZTNjNWE3NzNlZTNlMzc3ZDNmZDhmMjE4ODEwYTNmYjAzZWZhZDdjNzUxMzE2MDE4ODgzYzkiOiIweGYwMmRmZjJkZWIwYmQ5ODA3NmFmYjNmOTFiOGI1ZjYzZGI2N2UzYWQ2OGFkMjEwNGZiOGU3MGRmYjYwM2NjMjcifX19fSwidHlwZSI6IlRyYW5zZmVyIn19"}},"sender":{"type":"test","data":"0x11342d7d8ae3c7ead6845ade6838864c64197ec6a3f9ea8b6e91d6e95cbd1b80"},"nonce":"2125693044916249229","recipient":{"type":"test","data":"0x9fd4beb19363aa85863235445062bc37955561c4328b138fa74d4659cc8187d0"},"values":{"30810":{"nfts":{"9c394d17b2b6b9dc5b571898916f40d9ceb9646e51cf41acece5484399501aa6":["0x6a89bf4c7ada3248a507620e85bc30f0473b26d707bde1debd109ce3ca1bd94d","0xf1665d81caab813a29d91f52128c8d8b709d60b42e078a9ab861b9ad4d312805","0xfe7734c3db19d3f9cf65a2b68e5f2b2b9cdb981e27dbc54dded4b4f6e7805715"]}},"37676":{"fungibles":{"7c9de034fab6eb957d3f73f9e9e168a89a7e1ddfb31c5429f1441d4dd0f4b940":"0x949c1867c7fde0a6f81e2f62caacd9a11a5232fbfac9bd3fa90e9eb18f43963"}},"46832":{"fungibles":{"4319e8f196ce3c5a773ee3e377d3fd8f218810a3fb03efad7c751316018883c9":"0xf02dff2deb0bd98076afb3f91b8b5f63db67e3ad68ad2104fb8e70dfb603cc27"}}}}'; + '{"id":"an-id","data":{"type":"SubscribePhaseShifts","data":{"cancel": false}}}'; \ No newline at end of file