From c4d3a25c58677685d44477c1e21d626db7d01f5c Mon Sep 17 00:00:00 2001 From: Brendon Votteler Date: Thu, 4 Jul 2024 13:09:04 +1000 Subject: [PATCH] feat: add phala adapter and starter configs --- .github/workflows/xcm-tests.yml | 1 + scripts/configs/phala.yml | 26 +++ scripts/interlay-chopsticks-test.ts | 6 +- src/adapters/interlay.ts | 15 ++ src/adapters/phala.ts | 281 ++++++++++++++++++++++++++ src/configs/chains/polkadot-chains.ts | 8 + 6 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 scripts/configs/phala.yml create mode 100644 src/adapters/phala.ts diff --git a/.github/workflows/xcm-tests.yml b/.github/workflows/xcm-tests.yml index b831c399..2acfbf9b 100644 --- a/.github/workflows/xcm-tests.yml +++ b/.github/workflows/xcm-tests.yml @@ -72,6 +72,7 @@ jobs: -p scripts/configs/acala.yml \ -p scripts/configs/astar.yml \ -p scripts/configs/bifrost-polkadot.yml \ + -p scripts/configs/phala.yml \ &> log.txt & echo "Waiting for log to show chopsticks is ready..." tail -f log.txt | grep -q "Connected parachains" diff --git a/scripts/configs/phala.yml b/scripts/configs/phala.yml new file mode 100644 index 00000000..c5970bd7 --- /dev/null +++ b/scripts/configs/phala.yml @@ -0,0 +1,26 @@ +endpoint: + - wss://api.phala.network/ws + - wss://phala-rpc.dwellir.com +mock-signature-host: true + +import-storage: + System: + Account: + - + - + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice + - providers: 1 + data: + free: '1000000000000000' + Assets: + Account: + - + - + - 14 # IBTC + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice + - balance: '1000000000000000' + - + - + - 13 # INTR + - 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY # Alice + - balance: '1000000000000000' \ No newline at end of file diff --git a/scripts/interlay-chopsticks-test.ts b/scripts/interlay-chopsticks-test.ts index 8420765e..0b554e53 100644 --- a/scripts/interlay-chopsticks-test.ts +++ b/scripts/interlay-chopsticks-test.ts @@ -6,8 +6,8 @@ import { InterlayAdapter } from "../src/adapters/interlay"; import { HydraAdapter } from "../src/adapters/hydradx"; import { AcalaAdapter } from "../src/adapters/acala"; import { AstarAdapter } from "../src/adapters/astar"; -// import { ParallelAdapter } from "../src/adapters/parallel"; import { BifrostPolkadotAdapter } from "../src/adapters/bifrost"; +import { PhalaAdapter } from "../src/adapters/phala"; import { BaseCrossChainAdapter } from "../src/base-chain-adapter"; import { RouterTestCase, runTestCasesAndExit } from "./chopsticks-test"; @@ -27,9 +27,9 @@ async function main(): Promise { hydra: { adapter: new HydraAdapter(), endpoints: ['ws://127.0.0.1:8001'] }, acala: { adapter: new AcalaAdapter(), endpoints: ['ws://127.0.0.1:8002'] }, astar: { adapter: new AstarAdapter(), endpoints: ['ws://127.0.0.1:8003'] }, - // parallel: { adapter: new ParallelAdapter(), endpoints: ['ws://127.0.0.1:8004'] }, bifrost_polkadot: { adapter: new BifrostPolkadotAdapter(), endpoints: ['ws://127.0.0.1:8004']}, - polkadot: { adapter: new PolkadotAdapter(), endpoints: ['ws://127.0.0.1:8005'] }, + phala: { adapter: new PhalaAdapter(), endpoints: ['ws://127.0.0.1:8005']}, + polkadot: { adapter: new PolkadotAdapter(), endpoints: ['ws://127.0.0.1:8006'] }, }; const skipCases: Partial[] = [ diff --git a/src/adapters/interlay.ts b/src/adapters/interlay.ts index 8367d781..86864a12 100644 --- a/src/adapters/interlay.ts +++ b/src/adapters/interlay.ts @@ -65,6 +65,21 @@ export const interlayRoutersConfig: Omit[] = [ // during chopsticks test: fee = 103 Add 10x margin xcm: { fee: { token: "IBTC", amount: "1030" }, weightLimit: DEST_WEIGHT }, }, + { + to: "phala", + token: "INTR", + xcm: { + // TODO: get chopsticks test data: fee = 6_535_947_712 Add 10x margin + fee: { token: "INTR", amount: "65359477120" }, + weightLimit: DEST_WEIGHT, + }, + }, + { + to: "phala", + token: "IBTC", + // TODO: get chopsticks test: fee = 103 Add 10x margin + xcm: { fee: { token: "IBTC", amount: "1030" }, weightLimit: DEST_WEIGHT }, + }, { to: "polkadot", token: "DOT", diff --git a/src/adapters/phala.ts b/src/adapters/phala.ts new file mode 100644 index 00000000..98facad0 --- /dev/null +++ b/src/adapters/phala.ts @@ -0,0 +1,281 @@ +import { Storage } from "@acala-network/sdk/utils/storage"; +import { AnyApi, FixedPointNumber as FN } from "@acala-network/sdk-core"; +import { combineLatest, map, Observable } from "rxjs"; + +import { SubmittableExtrinsic } from "@polkadot/api/types"; +import { DeriveBalancesAll } from "@polkadot/api-derive/balances/types"; +import { ISubmittableResult } from "@polkadot/types/types"; + +import { BalanceAdapter, BalanceAdapterConfigs } from "../balance-adapter"; +import { BaseCrossChainAdapter } from "../base-chain-adapter"; +import { ChainName, chains } from "../configs"; +import { ApiNotFound, CurrencyNotFound } from "../errors"; +import { + BalanceData, + CrossChainRouterConfigs, + CrossChainTransferParams, + ExtendedToken, +} from "../types"; + +const DEST_WEIGHT = "Unlimited"; +type TokenData = ExtendedToken & { toQuery: () => string }; + +export const phalaRoutersConfig: Omit[] = [ + { + to: "interlay", + token: "IBTC", + xcm: { + // during chopsticks test: fee = 71 Add 10x margin + fee: { token: "IBTC", amount: "710" }, + weightLimit: DEST_WEIGHT, + }, + }, + { + to: "interlay", + token: "INTR", + xcm: { + // during chopsticks test: fee = 21_660_472 Add 10x margin + fee: { token: "INTR", amount: "216604720" }, + weightLimit: DEST_WEIGHT, + }, + }, +]; + +export const phalaTokensConfig: Record< + string, + Record +> = { + phala: { + PHA: { + name: "PHA", + symbol: "PHA", + decimals: 12, + ed: "10000000000", + } as TokenData, + // ed confirmed via assets.asset() + IBTC: { + name: "IBTC", + symbol: "IBTC", + decimals: 8, + ed: "1,000,000", + toRaw: () => + "0x0001000000000000000000000000000000000000000000000000000000000000", + toQuery: () => "14", + }, + INTR: { + name: "INTR", + symbol: "INTR", + decimals: 10, + ed: "100,000,000", + toRaw: () => + "0x0002000000000000000000000000000000000000000000000000000000000000", + toQuery: () => "13", + }, + }, +}; + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +const createBalanceStorages = (api: AnyApi) => { + return { + balances: (address: string) => + Storage.create({ + api, + path: "derive.balances.all", + params: [address], + }), + assets: (tokenId: string, address: string) => + Storage.create({ + api, + path: "query.assets.account", + params: [tokenId, address], + }), + }; +}; + +class PhalaBalanceAdapter extends BalanceAdapter { + private storages: ReturnType; + + constructor({ api, chain, tokens }: BalanceAdapterConfigs) { + super({ api, chain, tokens }); + this.storages = createBalanceStorages(api); + } + + public subscribeBalance( + token: string, + address: string + ): Observable { + const storage = this.storages.balances(address); + + if (token === this.nativeToken) { + return storage.observable.pipe( + map((data) => ({ + free: FN.fromInner(data.freeBalance.toString(), this.decimals), + locked: FN.fromInner(data.lockedBalance.toString(), this.decimals), + reserved: FN.fromInner( + data.reservedBalance.toString(), + this.decimals + ), + available: FN.fromInner( + data.availableBalance.toString(), + this.decimals + ), + })) + ); + } + + const tokenData: TokenData = this.getToken(token); + + if (!tokenData) { + throw new CurrencyNotFound(token); + } + + return this.storages.assets(tokenData.toQuery(), address).observable.pipe( + map((balance) => { + const amount = FN.fromInner( + balance.unwrapOrDefault()?.balance?.toString() || "0", + this.getToken(token).decimals + ); + + return { + free: amount, + locked: new FN(0), + reserved: new FN(0), + available: amount, + }; + }) + ); + } +} + +class BasePhalaAdapter extends BaseCrossChainAdapter { + private balanceAdapter?: PhalaBalanceAdapter; + + public override async setApi(api: AnyApi) { + this.api = api; + + await api.isReady; + + const chain = this.chain.id as ChainName; + + this.balanceAdapter = new PhalaBalanceAdapter({ + chain, + api, + tokens: phalaTokensConfig[chain], + }); + } + + public subscribeTokenBalance( + token: string, + address: string + ): Observable { + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id); + } + + return this.balanceAdapter.subscribeBalance(token, address); + } + + public subscribeMaxInput( + token: string, + address: string, + to: ChainName + ): Observable { + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id); + } + + return combineLatest({ + txFee: + token === this.balanceAdapter?.nativeToken + ? this.estimateTxFee({ + amount: FN.ZERO, + to, + token, + address, + signer: address, + }) + : "0", + balance: this.balanceAdapter + .subscribeBalance(token, address) + .pipe(map((i) => i.available)), + }).pipe( + map(({ balance, txFee }) => { + const tokenMeta = this.balanceAdapter?.getToken(token); + const feeFactor = 1.2; + const fee = FN.fromInner(txFee, tokenMeta?.decimals).mul( + new FN(feeFactor) + ); + + // always minus ed + return balance + .minus(fee) + .minus(FN.fromInner(tokenMeta?.ed || "0", tokenMeta?.decimals)); + }) + ); + } + + public createTx( + params: CrossChainTransferParams + ): + | SubmittableExtrinsic<"promise", ISubmittableResult> + | SubmittableExtrinsic<"rxjs", ISubmittableResult> { + if (!this.api) { + throw new ApiNotFound(this.chain.id); + } + + const { address, amount, to, token } = params; + + const accountId = this.api?.createType("AccountId32", address).toHex(); + const toChain = chains[to]; + + const dst = { + parents: 1, + interior: { + X2: [ + { Parachain: toChain.paraChainId }, + { AccountId32: { id: accountId } }, + ], + }, + }; + + let asset: any = { + id: { Concrete: { parents: 0, interior: "Here" } }, + fun: { Fungible: amount.toChainData() }, + }; + + const tokenData: TokenData = this.getToken(token); + + if (!tokenData) { + throw new CurrencyNotFound(token); + } + + if (token !== this.balanceAdapter?.nativeToken) { + asset = { + id: { + Concrete: { + parents: 1, + interior: { + X2: [ + { Parachain: toChain.paraChainId }, + { GeneralKey: { length: 2, data: tokenData.toRaw() } }, + ], + }, + }, + }, + fun: { Fungible: amount.toChainData() }, + }; + } + + return this.api.tx.xTransfer.transfer(asset, dst, undefined); + } +} + +export class PhalaAdapter extends BasePhalaAdapter { + constructor() { + super( + chains.phala, + phalaRoutersConfig, + phalaTokensConfig.parallel + ); + } +} diff --git a/src/configs/chains/polkadot-chains.ts b/src/configs/chains/polkadot-chains.ts index 36447747..08d412ef 100644 --- a/src/configs/chains/polkadot-chains.ts +++ b/src/configs/chains/polkadot-chains.ts @@ -77,4 +77,12 @@ export const polkadotChains = { paraChainId: 2034, ss58Prefix: 63, }, + phala: { + id: "phala", + display: "Phala", + type: typeSubstrate, + icon: "", + paraChainId: 2035, + ss58Prefix: 30, + }, };