diff --git a/examples/assets.eg.ts b/examples/assets.eg.ts new file mode 100644 index 000000000..f1f12c9b3 --- /dev/null +++ b/examples/assets.eg.ts @@ -0,0 +1,181 @@ +/** + * @title Asset Palette Example + * @stability unstable + * @description Various calls utilizing the asset palette + */ + +import { contractsDev } from "@capi/contracts-dev" +import { assertEquals, assertRejects } from "asserts" +import { createDevUsers, is, Rune, Scope } from "capi" +import { signature } from "capi/patterns/signature/polkadot" + +const scope = new Scope() + +const { alexa, billy, carol } = await createDevUsers() + +const ASSET_ID = 0 +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +await contractsDev.Assets.create({ + id: ASSET_ID, + admin: alexa.address, + minBalance: 1n, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Create Asset:") + .inBlockEvents() + .run(scope) + +await contractsDev.Assets.setMetadata({ + id: ASSET_ID, + name: textEncoder.encode("Capi Socks"), + symbol: textEncoder.encode("CAPI"), + decimals: 2, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Set Metadata:") + .inBlockEvents() + .run(scope) + +await contractsDev.Assets.Metadata.value(ASSET_ID, contractsDev.latestBlockHash) + .unhandle(is(undefined)) + .map((metadata) => ({ + ...metadata, + name: textDecoder.decode(metadata.name), + symbol: textDecoder.decode(metadata.symbol), + })) + .dbg("Asset Metadata:") + .run(scope) + +await contractsDev.Assets.mint({ + id: ASSET_ID, + beneficiary: alexa.address, + amount: 1000n, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Mint Asset:") + .inBlockEvents() + .unhandleFailed() + .run(scope) + +await contractsDev.Assets.transfer({ + id: ASSET_ID, + target: billy.address, + amount: 10n, +}) + .signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Transfer Asset to Billy:") + .inBlockEvents() + .unhandleFailed() + .run(scope) + +await contractsDev.Assets.Account + .value(Rune.tuple([ASSET_ID, billy.publicKey]), contractsDev.latestBlockHash) + .unhandle(is(undefined)) + .map(({ balance }) => balance) + .dbg("Billy Asset Balance:") + .run(scope) + +await contractsDev.Assets.burn({ + id: ASSET_ID, + who: billy.address, + amount: 5n, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Burn Billy's Assets:") + .inBlockEvents() + .run(scope) + +await contractsDev.Assets.Account + .value(Rune.tuple([ASSET_ID, billy.publicKey]), contractsDev.latestBlockHash) + .unhandle(is(undefined)) + .map(({ balance }) => balance) + .dbg("Billy Asset Balance Post Burn:") + .run(scope) + +await contractsDev.Assets.freeze({ + id: ASSET_ID, + who: billy.address, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Freeze Billy's Assets:") + .inBlockEvents() + .run(scope) + +await assertRejects(async () => + contractsDev.Assets.transfer({ + id: ASSET_ID, + target: carol.address, + amount: 1n, + }) + .signed(signature({ sender: billy })) + .sent() + .dbgStatus("Billy Transfer Frozen Assets:") + .inBlockEvents() + .unhandleFailed() + .run(scope), "Transfer Frozen Assets") + +await contractsDev.Assets.thaw({ + id: ASSET_ID, + who: billy.address, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Thaw Billy's Assets:") + .inBlockEvents() + .run(scope) + +await contractsDev.Assets.transfer({ + id: ASSET_ID, + target: carol.address, + amount: 1n, +}) + .signed(signature({ sender: billy })) + .sent() + .dbgStatus("Billy Transfer Thawed Assets:") + .inBlockEvents() + .unhandleFailed() + .run(scope) + +const carolBalance = await contractsDev.Assets.Account + .value(Rune.tuple([ASSET_ID, carol.publicKey]), contractsDev.latestBlockHash) + .unhandle(is(undefined)) + .map(({ balance }) => balance) + .dbg("Carol Asset Balance:") + .run(scope) + +assertEquals(carolBalance, 1n, "Carol Balance") + +await contractsDev.Utility.batchAll({ + calls: Rune.array([alexa.address, billy.address, carol.address]) + .mapArray((addr) => + contractsDev.Assets.burn({ + id: ASSET_ID, + who: addr, + amount: 100_000_000_000_000n, + }) + ), +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Burn Everything:") + .inBlockEvents() + .run(scope) + +await contractsDev.Assets.startDestroy({ + id: ASSET_ID, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Destroy Asset Class Start:") + .inBlockEvents() + .unhandleFailed() + .run(scope) + +await contractsDev.Assets.finishDestroy({ + id: ASSET_ID, +}).signed(signature({ sender: alexa })) + .sent() + .dbgStatus("Destroy Asset Class Finish:") + .inBlockEvents() + .unhandleFailed() + .run(scope) diff --git a/examples/dex.eg.ts b/examples/dex.eg.ts new file mode 100644 index 000000000..36ac713e2 --- /dev/null +++ b/examples/dex.eg.ts @@ -0,0 +1,208 @@ +import { localDev, NativeOrAssetId } from "@capi/local-dev" +import { alice, bob, is } from "capi" +import { signature } from "capi/patterns/signature/statemint" +import { + filterLiquidityAddedEvent, + filterPoolCreatedEvents, + getReserves, + quotePriceExactTokensForTokens, +} from "capi/patterns/unstable/dex" +import { Rune, Scope } from "../rune/Rune.ts" + +const scope = new Scope() + +const DOT_ASSET_ID = 0 +const USDT_ASSET_ID = 1 + +const textEncoder = new TextEncoder() +const textDecoder = new TextDecoder() + +await localDev.Assets.create({ + id: DOT_ASSET_ID, + admin: alice.address, + minBalance: 1n, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Create DOT Asset:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await localDev.Assets.setMetadata({ + id: DOT_ASSET_ID, + name: textEncoder.encode("Polkadot Token"), + symbol: textEncoder.encode("DOT"), + decimals: 2, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Set DOT Metadata:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await localDev.Assets.create({ + id: USDT_ASSET_ID, + admin: alice.address, + minBalance: 1n, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Create USDT Asset:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await localDev.Assets.setMetadata({ + id: USDT_ASSET_ID, + name: textEncoder.encode("United States Tether"), + symbol: textEncoder.encode("USDT"), + decimals: 2, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Set USDT Metadata:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +const poolId = await localDev.AssetConversion.createPool({ + asset1: NativeOrAssetId.Asset(DOT_ASSET_ID), + asset2: NativeOrAssetId.Asset(USDT_ASSET_ID), +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Create DOT/USDT Pool:") + .finalizedEvents() + .unhandleFailed() + .pipe(filterPoolCreatedEvents) + .access(0, "poolId") + .run(scope) + +await localDev.Assets.mint({ + id: DOT_ASSET_ID, + beneficiary: alice.address, + amount: 218173n, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Mint DOT to Alice:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await localDev.Assets.mint({ + id: USDT_ASSET_ID, + beneficiary: alice.address, + amount: 1000000n, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Mint USDT to Alice:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +const lpTokenAmount = await localDev.AssetConversion.addLiquidity({ + asset1: NativeOrAssetId.Asset(DOT_ASSET_ID), + asset2: NativeOrAssetId.Asset(USDT_ASSET_ID), + amount1Desired: 218173n, + amount2Desired: 1000000n, + amount1Min: 218173n, + amount2Min: 1000000n, + mintTo: alice.publicKey, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Add Liquidity to DOT/USDT Pool:") + .finalizedEvents() + .unhandleFailed() + .pipe(filterLiquidityAddedEvent) + .access(0, "lpTokenMinted") + .run(scope) + +await localDev.Assets.mint({ + id: USDT_ASSET_ID, + beneficiary: bob.address, + amount: 30000n, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Mint USDT to Bob:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await quotePriceExactTokensForTokens( + NativeOrAssetId.Asset(USDT_ASSET_ID), + NativeOrAssetId.Asset(DOT_ASSET_ID), + 30000n, + true, +) + .dbg("DOT/USDT Quote:") + .run(scope) + +await getReserves(NativeOrAssetId.Asset(USDT_ASSET_ID), NativeOrAssetId.Asset(DOT_ASSET_ID)) + .dbg("DOT/USDT Reserves Before:") + .run(scope) + +// swap with no slippage checks +await localDev.AssetConversion.swapExactTokensForTokens({ + path: Rune.tuple([NativeOrAssetId.Asset(USDT_ASSET_ID), NativeOrAssetId.Asset(DOT_ASSET_ID)]), + amountIn: 30000n, + amountOutMin: 1n, + sendTo: bob.publicKey, + keepAlive: false, +}).signed(signature({ sender: bob })) + .sent() + .dbgStatus("Bob Swap 30k USDT for DOT:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +const bobDotBalance = await localDev.Assets.Account + .value(Rune.tuple([DOT_ASSET_ID, bob.publicKey])) + .unhandle(is(undefined)) + .map(({ balance }) => balance) + .dbg("Bob's Dot Balance:") + .run(scope) + +await getReserves(NativeOrAssetId.Asset(USDT_ASSET_ID), NativeOrAssetId.Asset(DOT_ASSET_ID)) + .dbg("DOT/USDT Reserves After:") + .run(scope) + +await localDev.AssetConversion.removeLiquidity({ + asset1: NativeOrAssetId.Asset(DOT_ASSET_ID), + asset2: NativeOrAssetId.Asset(USDT_ASSET_ID), + lpTokenBurn: lpTokenAmount / 2n, + amount1MinReceive: 1n, + amount2MinReceive: 1n, + withdrawTo: alice.publicKey, +}).signed(signature({ sender: alice })) + .sent() + .dbgStatus("Remove Half Liquidity to DOT/USDT Pool:") + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await quotePriceExactTokensForTokens( + NativeOrAssetId.Asset(DOT_ASSET_ID), + NativeOrAssetId.Asset(USDT_ASSET_ID), + bobDotBalance, + true, +) + .dbg("USDT/DOT Quote:") + .run(scope) + +// Should not get back 30k - fees since liquidity was reduced +await localDev.AssetConversion.swapExactTokensForTokens({ + path: Rune.tuple([NativeOrAssetId.Asset(DOT_ASSET_ID), NativeOrAssetId.Asset(USDT_ASSET_ID)]), + amountIn: bobDotBalance, + amountOutMin: 1n, + sendTo: bob.publicKey, + keepAlive: false, +}).signed(signature({ sender: bob })) + .sent() + .dbgStatus(`Bob Swap ${bobDotBalance} USDT for DOT:`) + .finalizedEvents() + .unhandleFailed() + .run(scope) + +await localDev.Assets.Account + .value(Rune.tuple([USDT_ASSET_ID, bob.publicKey])) + .unhandle(is(undefined)) + .map(({ balance }) => balance) + .dbg("Bob's USDT Balance:") + .run(scope) diff --git a/import_map.json b/import_map.json index 12f7e883d..08497c8f6 100644 --- a/import_map.json +++ b/import_map.json @@ -1,6 +1,6 @@ { "imports": { - "@capi/": "http://localhost:4646/d5de72ca4087cd41/" + "@capi/": "http://localhost:4646/353331e84ae4dc5a/" }, "scopes": { "examples/": { @@ -12,6 +12,7 @@ "capi/patterns/signature/polkadot": "./patterns/signature/polkadot.ts", "capi/patterns/signature/statemint": "./patterns/signature/statemint.ts", "capi/patterns/unstable/consensus": "./patterns/unstable/consensus/mod.ts", + "capi/patterns/unstable/dex": "./patterns/unstable/dex/mod.ts", "capi/patterns/unstable/identity": "./patterns/unstable/identity.ts", "capi/patterns/unstable/ink": "./patterns/unstable/ink/mod.ts", "capi/patterns/unstable/multisig": "./patterns/unstable/multisig/mod.ts", diff --git a/nets.ts b/nets.ts index 678798290..412a74262 100644 --- a/nets.ts +++ b/nets.ts @@ -20,6 +20,9 @@ export const polkadot = net.ws({ export const westend = net.ws({ url: "wss://westend-rpc.polkadot.io/" }) export const statemint = net.ws({ url: "wss://statemint-rpc.polkadot.io/" }) +export const rococoAssetHub = net.ws({ url: "wss://rococo-asset-hub-rpc.polkadot.io/" }) +export const westmint = net.ws({ url: "wss://westmint-rpc.polkadot.io/" }) +export const localDev = net.ws({ url: "ws://127.0.0.1:9944" }) export const westendDev = net.dev({ bin: bin.polkadot, diff --git a/patterns/unstable/dex/events.ts b/patterns/unstable/dex/events.ts new file mode 100644 index 000000000..be2472318 --- /dev/null +++ b/patterns/unstable/dex/events.ts @@ -0,0 +1,24 @@ +import { PalletAssetConversionEvent } from "@capi/local-dev" +import { Rune, RunicArgs } from "../../../mod.ts" + +export function filterPoolCreatedEvents(...[events]: RunicArgs) { + return Rune.resolve(events).map((events) => + events + .map((e) => e.event) + .map((e) => e.value) + .filter((event): event is PalletAssetConversionEvent.PoolCreated => + event.type === "PoolCreated" + ) + ) +} + +export function filterLiquidityAddedEvent(...[events]: RunicArgs) { + return Rune.resolve(events).map((events) => + events + .map((e) => e.event) + .map((e) => e.value) + .filter((event): event is PalletAssetConversionEvent.LiquidityAdded => + event.type === "LiquidityAdded" + ) + ) +} diff --git a/patterns/unstable/dex/mod.ts b/patterns/unstable/dex/mod.ts new file mode 100644 index 000000000..0e2b90465 --- /dev/null +++ b/patterns/unstable/dex/mod.ts @@ -0,0 +1,2 @@ +export * from "./events.ts" +export * from "./runtime.ts" diff --git a/patterns/unstable/dex/runtime.ts b/patterns/unstable/dex/runtime.ts new file mode 100644 index 000000000..74a08d72b --- /dev/null +++ b/patterns/unstable/dex/runtime.ts @@ -0,0 +1,64 @@ +import { localDev, NativeOrAssetId } from "@capi/local-dev" +import { $, CodecRune, hex, Rune, RunicArgs } from "../../../mod.ts" + +export function quotePriceExactTokensForTokens( + ...[asset1, asset2, amount, includeFee]: RunicArgs< + X, + [asset1: NativeOrAssetId, asset2: NativeOrAssetId, amount: bigint, includeFee: boolean] + > +) { + const $assetId = localDev.metadata.access( + "paths", + "pallet_asset_conversion::types::NativeOrAssetId", + ) + + const args = $assetId.map(($assetId) => $.tuple($assetId, $assetId, $.u128, $.bool)) + .into(CodecRune) + .encoded( + Rune.tuple([ + asset1, + asset2, + amount, + includeFee, + ]), + ) + + return localDev.connection + .call( + "state_call", + "AssetConversionApi_quote_price_exact_tokens_for_tokens", + args.map(hex.encode), + ) + .map(hex.decode) + .map((x) => $.u128.decode(x)) +} + +export function getReserves( + ...[asset1, asset2]: RunicArgs< + X, + [asset1: NativeOrAssetId, asset2: NativeOrAssetId] + > +) { + const $assetId = localDev.metadata.access( + "paths", + "pallet_asset_conversion::types::NativeOrAssetId", + ) + + const args = $assetId.map(($assetId) => $.tuple($assetId, $assetId)) + .into(CodecRune) + .encoded( + Rune.tuple([ + asset1, + asset2, + ]), + ) + + return localDev.connection + .call( + "state_call", + "AssetConversionApi_get_reserves", + args.map(hex.encode), + ) + .map(hex.decode) + .map((x) => $.tuple($.u128, $.u128).decode(x)) +}