From 5b65e8ceee4b43550c33a934308193331b484cc8 Mon Sep 17 00:00:00 2001 From: veeso Date: Tue, 20 Feb 2024 17:11:55 +0100 Subject: [PATCH] feat: ekoke-index canister --- Cargo.lock | 19 ++ Cargo.toml | 1 + Makefile.toml | 8 +- dfx.json | 5 + scripts/dfx-setup.sh | 10 + .../ekoke-index/ekoke-index.did.d.ts | 80 ++++++ .../ekoke-index/ekoke-index.did.js | 87 ++++++ src/declarations/ekoke-index/index.d.ts | 50 ++++ src/declarations/ekoke-index/index.js | 43 +++ .../ekoke-ledger/ekoke-ledger.did.d.ts | 27 +- .../ekoke-ledger/ekoke-ledger.did.js | 26 +- src/did/src/ekoke_index.rs | 40 +++ src/did/src/ekoke_index/transaction.rs | 148 ++++++++++ src/did/src/lib.rs | 1 + src/ekoke_index/Cargo.toml | 35 +++ src/ekoke_index/ekoke-index.did | 61 +++++ src/ekoke_index/src/app.rs | 115 ++++++++ src/ekoke_index/src/app/configuration.rs | 56 ++++ src/ekoke_index/src/app/index.rs | 255 ++++++++++++++++++ src/ekoke_index/src/app/inspect.rs | 34 +++ src/ekoke_index/src/app/memory.rs | 15 ++ src/ekoke_index/src/app/test_utils.rs | 58 ++++ src/ekoke_index/src/inspect.rs | 31 +++ src/ekoke_index/src/lib.rs | 60 +++++ src/ekoke_index/src/utils.rs | 13 + src/ekoke_ledger/ekoke-ledger.did | 26 +- src/ekoke_ledger/src/app.rs | 28 -- src/ekoke_ledger/src/app/register.rs | 51 +--- src/ekoke_ledger/src/lib.rs | 9 +- 29 files changed, 1250 insertions(+), 142 deletions(-) create mode 100755 scripts/dfx-setup.sh create mode 100644 src/declarations/ekoke-index/ekoke-index.did.d.ts create mode 100644 src/declarations/ekoke-index/ekoke-index.did.js create mode 100644 src/declarations/ekoke-index/index.d.ts create mode 100644 src/declarations/ekoke-index/index.js create mode 100644 src/did/src/ekoke_index.rs create mode 100644 src/did/src/ekoke_index/transaction.rs create mode 100644 src/ekoke_index/Cargo.toml create mode 100644 src/ekoke_index/ekoke-index.did create mode 100644 src/ekoke_index/src/app.rs create mode 100644 src/ekoke_index/src/app/configuration.rs create mode 100644 src/ekoke_index/src/app/index.rs create mode 100644 src/ekoke_index/src/app/inspect.rs create mode 100644 src/ekoke_index/src/app/memory.rs create mode 100644 src/ekoke_index/src/app/test_utils.rs create mode 100644 src/ekoke_index/src/inspect.rs create mode 100644 src/ekoke_index/src/lib.rs create mode 100644 src/ekoke_index/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index a086e88..d6a24a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,25 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "ekoke_index" +version = "0.1.0" +dependencies = [ + "candid", + "did", + "ic-cdk", + "ic-cdk-macros", + "ic-stable-structures", + "icrc", + "num-bigint", + "num-traits", + "pretty_assertions", + "rand", + "serde", + "thiserror", + "tokio", +] + [[package]] name = "ekoke_ledger" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 1b9ad74..5a52f40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "src/did", "src/dip721", "src/deferred", + "src/ekoke_index", "src/ekoke_ledger", "src/icrc", "src/marketplace", diff --git a/Makefile.toml b/Makefile.toml index ffdb387..7c44bf8 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -54,6 +54,7 @@ args = ["fmt", "--all", "--", "--check"] description = "Generate did files" dependencies = [ "deferred-did", + "ekoke-index-did", "ekoke-ledger-did", "marketplace-did", "dfx-generate", @@ -65,6 +66,11 @@ description = "Generate did files for deferred" script = "cargo run --bin deferred-did --features did > src/deferred/deferred.did" workspace = false +[tasks.ekoke-index-did] +description = "Generate did files for ekoke-index" +script = "cargo run --bin ekoke-index-did --features did > src/ekoke_index/ekoke-index.did" +workspace = false + [tasks.ekoke-ledger-did] description = "Generate did files for ekoke-ledger" script = "cargo run --bin ekoke-ledger-did --features did > src/ekoke_ledger/ekoke-ledger.did" @@ -83,5 +89,5 @@ workspace = false [tasks.dfx-setup] description = "setup dfx" -script = "dfx stop; dfx start --background; dfx canister create deferred; dfx canister create ekoke-ledger; dfx canister create marketplace" +script = "./scripts/dfx-setup.sh" workspace = false diff --git a/dfx.json b/dfx.json index d1d333a..ef12989 100644 --- a/dfx.json +++ b/dfx.json @@ -5,6 +5,11 @@ "package": "deferred", "type": "rust" }, + "ekoke-index": { + "candid": "src/ekoke_index/ekoke-index.did", + "package": "ekoke_index", + "type": "rust" + }, "ekoke-ledger": { "candid": "src/ekoke_ledger/ekoke-ledger.did", "package": "ekoke_ledger", diff --git a/scripts/dfx-setup.sh b/scripts/dfx-setup.sh new file mode 100755 index 0000000..64a92c4 --- /dev/null +++ b/scripts/dfx-setup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +dfx stop +dfx start --background +dfx canister create deferred +dfx canister create ekoke-index +dfx canister create ekoke-ledger +dfx canister create marketplace + +dfx stop diff --git a/src/declarations/ekoke-index/ekoke-index.did.d.ts b/src/declarations/ekoke-index/ekoke-index.did.d.ts new file mode 100644 index 0000000..15d2e9a --- /dev/null +++ b/src/declarations/ekoke-index/ekoke-index.did.d.ts @@ -0,0 +1,80 @@ +import type { Principal } from '@dfinity/principal'; +import type { ActorMethod } from '@dfinity/agent'; +import type { IDL } from '@dfinity/candid'; + +export interface Account { + 'owner' : Principal, + 'subaccount' : [] | [Uint8Array | number[]], +} +export interface Approve { + 'fee' : [] | [bigint], + 'from' : Account, + 'memo' : [] | [Uint8Array | number[]], + 'created_at_time' : [] | [bigint], + 'amount' : bigint, + 'expected_allowance' : [] | [bigint], + 'expires_at' : [] | [bigint], + 'spender' : [] | [Account], +} +export interface Burn { + 'from' : Account, + 'memo' : [] | [Uint8Array | number[]], + 'created_at_time' : [] | [bigint], + 'amount' : bigint, + 'spender' : [] | [Account], +} +export interface EkokeIndexInitData { 'ledger_id' : Principal } +export interface GetAccountTransactionArgs { + 'max_results' : bigint, + 'start' : [] | [bigint], + 'account' : Account, +} +export interface GetTransactions { + 'transactions' : Array, + 'oldest_tx_id' : [] | [bigint], +} +export interface GetTransactionsErr { 'message' : string } +export interface ListSubaccountsArgs { + 'owner' : Principal, + 'start' : [] | [Uint8Array | number[]], +} +export interface Mint { + 'to' : Account, + 'memo' : [] | [Uint8Array | number[]], + 'created_at_time' : [] | [bigint], + 'amount' : bigint, +} +export type Result = { 'Ok' : GetTransactions } | + { 'Err' : GetTransactionsErr }; +export interface Transaction { + 'burn' : [] | [Burn], + 'kind' : string, + 'mint' : [] | [Mint], + 'approve' : [] | [Approve], + 'timestamp' : bigint, + 'transfer' : [] | [Transfer], +} +export interface TransactionWithId { + 'id' : bigint, + 'transaction' : Transaction, +} +export interface Transfer { + 'to' : Account, + 'fee' : [] | [bigint], + 'from' : Account, + 'memo' : [] | [Uint8Array | number[]], + 'created_at_time' : [] | [bigint], + 'amount' : bigint, + 'spender' : [] | [Account], +} +export interface _SERVICE { + 'commit' : ActorMethod<[Transaction], bigint>, + 'get_account_transactions' : ActorMethod<[GetAccountTransactionArgs], Result>, + 'ledger_id' : ActorMethod<[], Principal>, + 'list_subaccounts' : ActorMethod< + [ListSubaccountsArgs], + Array + >, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: ({ IDL }: { IDL: IDL }) => IDL.Type[]; diff --git a/src/declarations/ekoke-index/ekoke-index.did.js b/src/declarations/ekoke-index/ekoke-index.did.js new file mode 100644 index 0000000..496d93c --- /dev/null +++ b/src/declarations/ekoke-index/ekoke-index.did.js @@ -0,0 +1,87 @@ +export const idlFactory = ({ IDL }) => { + const EkokeIndexInitData = IDL.Record({ 'ledger_id' : IDL.Principal }); + const Account = IDL.Record({ + 'owner' : IDL.Principal, + 'subaccount' : IDL.Opt(IDL.Vec(IDL.Nat8)), + }); + const Burn = IDL.Record({ + 'from' : Account, + 'memo' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'created_at_time' : IDL.Opt(IDL.Nat64), + 'amount' : IDL.Nat, + 'spender' : IDL.Opt(Account), + }); + const Mint = IDL.Record({ + 'to' : Account, + 'memo' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'created_at_time' : IDL.Opt(IDL.Nat64), + 'amount' : IDL.Nat, + }); + const Approve = IDL.Record({ + 'fee' : IDL.Opt(IDL.Nat), + 'from' : Account, + 'memo' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'created_at_time' : IDL.Opt(IDL.Nat64), + 'amount' : IDL.Nat, + 'expected_allowance' : IDL.Opt(IDL.Nat), + 'expires_at' : IDL.Opt(IDL.Nat64), + 'spender' : IDL.Opt(Account), + }); + const Transfer = IDL.Record({ + 'to' : Account, + 'fee' : IDL.Opt(IDL.Nat), + 'from' : Account, + 'memo' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'created_at_time' : IDL.Opt(IDL.Nat64), + 'amount' : IDL.Nat, + 'spender' : IDL.Opt(Account), + }); + const Transaction = IDL.Record({ + 'burn' : IDL.Opt(Burn), + 'kind' : IDL.Text, + 'mint' : IDL.Opt(Mint), + 'approve' : IDL.Opt(Approve), + 'timestamp' : IDL.Nat64, + 'transfer' : IDL.Opt(Transfer), + }); + const GetAccountTransactionArgs = IDL.Record({ + 'max_results' : IDL.Nat, + 'start' : IDL.Opt(IDL.Nat), + 'account' : Account, + }); + const TransactionWithId = IDL.Record({ + 'id' : IDL.Nat, + 'transaction' : Transaction, + }); + const GetTransactions = IDL.Record({ + 'transactions' : IDL.Vec(TransactionWithId), + 'oldest_tx_id' : IDL.Opt(IDL.Nat), + }); + const GetTransactionsErr = IDL.Record({ 'message' : IDL.Text }); + const Result = IDL.Variant({ + 'Ok' : GetTransactions, + 'Err' : GetTransactionsErr, + }); + const ListSubaccountsArgs = IDL.Record({ + 'owner' : IDL.Principal, + 'start' : IDL.Opt(IDL.Vec(IDL.Nat8)), + }); + return IDL.Service({ + 'commit' : IDL.Func([Transaction], [IDL.Nat], []), + 'get_account_transactions' : IDL.Func( + [GetAccountTransactionArgs], + [Result], + [], + ), + 'ledger_id' : IDL.Func([], [IDL.Principal], ['query']), + 'list_subaccounts' : IDL.Func( + [ListSubaccountsArgs], + [IDL.Vec(IDL.Vec(IDL.Nat8))], + ['query'], + ), + }); +}; +export const init = ({ IDL }) => { + const EkokeIndexInitData = IDL.Record({ 'ledger_id' : IDL.Principal }); + return [EkokeIndexInitData]; +}; diff --git a/src/declarations/ekoke-index/index.d.ts b/src/declarations/ekoke-index/index.d.ts new file mode 100644 index 0000000..9b25914 --- /dev/null +++ b/src/declarations/ekoke-index/index.d.ts @@ -0,0 +1,50 @@ +import type { + ActorSubclass, + HttpAgentOptions, + ActorConfig, + Agent, +} from "@dfinity/agent"; +import type { Principal } from "@dfinity/principal"; +import type { IDL } from "@dfinity/candid"; + +import { _SERVICE } from './ekoke-index.did'; + +export declare const idlFactory: IDL.InterfaceFactory; +export declare const canisterId: string; + +export declare interface CreateActorOptions { + /** + * @see {@link Agent} + */ + agent?: Agent; + /** + * @see {@link HttpAgentOptions} + */ + agentOptions?: HttpAgentOptions; + /** + * @see {@link ActorConfig} + */ + actorOptions?: ActorConfig; +} + +/** + * Intializes an {@link ActorSubclass}, configured with the provided SERVICE interface of a canister. + * @constructs {@link ActorSubClass} + * @param {string | Principal} canisterId - ID of the canister the {@link Actor} will talk to + * @param {CreateActorOptions} options - see {@link CreateActorOptions} + * @param {CreateActorOptions["agent"]} options.agent - a pre-configured agent you'd like to use. Supercedes agentOptions + * @param {CreateActorOptions["agentOptions"]} options.agentOptions - options to set up a new agent + * @see {@link HttpAgentOptions} + * @param {CreateActorOptions["actorOptions"]} options.actorOptions - options for the Actor + * @see {@link ActorConfig} + */ +export declare const createActor: ( + canisterId: string | Principal, + options?: CreateActorOptions +) => ActorSubclass<_SERVICE>; + +/** + * Intialized Actor using default settings, ready to talk to a canister using its candid interface + * @constructs {@link ActorSubClass} + */ +export declare const ekoke-index: ActorSubclass<_SERVICE>; diff --git a/src/declarations/ekoke-index/index.js b/src/declarations/ekoke-index/index.js new file mode 100644 index 0000000..968b587 --- /dev/null +++ b/src/declarations/ekoke-index/index.js @@ -0,0 +1,43 @@ +import { Actor, HttpAgent } from "@dfinity/agent"; + +// Imports and re-exports candid interface +import { idlFactory } from "./ekoke-index.did.js"; +export { idlFactory } from "./ekoke-index.did.js"; + +/* CANISTER_ID is replaced by webpack based on node environment + * Note: canister environment variable will be standardized as + * process.env.CANISTER_ID_ + * beginning in dfx 0.15.0 + */ +export const canisterId = + process.env.CANISTER_ID_EKOKE-INDEX || + process.env.EKOKE-INDEX_CANISTER_ID; + +export const createActor = (canisterId, options = {}) => { + const agent = options.agent || new HttpAgent({ ...options.agentOptions }); + + if (options.agent && options.agentOptions) { + console.warn( + "Detected both agent and agentOptions passed to createActor. Ignoring agentOptions and proceeding with the provided agent." + ); + } + + // Fetch root key for certificate validation during development + if (process.env.DFX_NETWORK !== "ic") { + agent.fetchRootKey().catch((err) => { + console.warn( + "Unable to fetch root key. Check to ensure that your local replica is running" + ); + console.error(err); + }); + } + + // Creates an actor with using the candid interface and the HttpAgent + return Actor.createActor(idlFactory, { + agent, + canisterId, + ...options.actorOptions, + }); +}; + +export const ekoke-index = canisterId ? createActor(canisterId) : undefined; diff --git a/src/declarations/ekoke-ledger/ekoke-ledger.did.d.ts b/src/declarations/ekoke-ledger/ekoke-ledger.did.d.ts index 0006129..38d146f 100644 --- a/src/declarations/ekoke-ledger/ekoke-ledger.did.d.ts +++ b/src/declarations/ekoke-ledger/ekoke-ledger.did.d.ts @@ -120,28 +120,18 @@ export type Result_2 = { 'Ok' : bigint } | { 'Err' : EkokeError }; export type Result_3 = { 'Ok' : bigint } | { 'Err' : EkokeError }; -export type Result_4 = { 'Ok' : Transaction } | - { 'Err' : EkokeError }; -export type Result_5 = { 'Ok' : bigint } | +export type Result_4 = { 'Ok' : bigint } | { 'Err' : TransferError }; -export type Result_6 = { 'Ok' : bigint } | +export type Result_5 = { 'Ok' : bigint } | { 'Err' : ApproveError }; -export type Result_7 = { 'Ok' : bigint } | +export type Result_6 = { 'Ok' : bigint } | { 'Err' : TransferFromError }; -export type Result_8 = { 'Ok' : LiquidityPoolBalance } | +export type Result_7 = { 'Ok' : LiquidityPoolBalance } | { 'Err' : EkokeError }; export type Role = { 'DeferredCanister' : null } | { 'MarketplaceCanister' : null } | { 'Admin' : null }; export interface TokenExtension { 'url' : string, 'name' : string } -export interface Transaction { - 'to' : Account, - 'fee' : bigint, - 'from' : Account, - 'memo' : [] | [Uint8Array | number[]], - 'created_at' : bigint, - 'amount' : bigint, -} export interface TransferArg { 'to' : Account, 'fee' : [] | [bigint], @@ -204,7 +194,6 @@ export interface _SERVICE { >, 'erc20_swap_fee' : ActorMethod<[], Result_2>, 'get_contract_reward' : ActorMethod<[bigint, bigint], Result_3>, - 'get_transaction' : ActorMethod<[bigint], Result_4>, 'http_request' : ActorMethod<[HttpRequest], HttpResponse>, 'http_transform_send_tx' : ActorMethod<[TransformArgs], HttpResponse_1>, 'icrc1_balance_of' : ActorMethod<[Account], bigint>, @@ -215,12 +204,12 @@ export interface _SERVICE { 'icrc1_supported_standards' : ActorMethod<[], Array>, 'icrc1_symbol' : ActorMethod<[], string>, 'icrc1_total_supply' : ActorMethod<[], bigint>, - 'icrc1_transfer' : ActorMethod<[TransferArg], Result_5>, + 'icrc1_transfer' : ActorMethod<[TransferArg], Result_4>, 'icrc2_allowance' : ActorMethod<[AllowanceArgs], Allowance>, - 'icrc2_approve' : ActorMethod<[ApproveArgs], Result_6>, - 'icrc2_transfer_from' : ActorMethod<[TransferFromArgs], Result_7>, + 'icrc2_approve' : ActorMethod<[ApproveArgs], Result_5>, + 'icrc2_transfer_from' : ActorMethod<[TransferFromArgs], Result_6>, 'liquidity_pool_accounts' : ActorMethod<[], LiquidityPoolAccounts>, - 'liquidity_pool_balance' : ActorMethod<[], Result_8>, + 'liquidity_pool_balance' : ActorMethod<[], Result_7>, 'reserve_pool' : ActorMethod< [bigint, bigint, [] | [Uint8Array | number[]]], Result_3 diff --git a/src/declarations/ekoke-ledger/ekoke-ledger.did.js b/src/declarations/ekoke-ledger/ekoke-ledger.did.js index 3ab264b..123df14 100644 --- a/src/declarations/ekoke-ledger/ekoke-ledger.did.js +++ b/src/declarations/ekoke-ledger/ekoke-ledger.did.js @@ -125,15 +125,6 @@ export const idlFactory = ({ IDL }) => { const Result_1 = IDL.Variant({ 'Ok' : IDL.Text, 'Err' : EkokeError }); const Result_2 = IDL.Variant({ 'Ok' : IDL.Nat64, 'Err' : EkokeError }); const Result_3 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : EkokeError }); - const Transaction = IDL.Record({ - 'to' : Account, - 'fee' : IDL.Nat, - 'from' : Account, - 'memo' : IDL.Opt(IDL.Vec(IDL.Nat8)), - 'created_at' : IDL.Nat64, - 'amount' : IDL.Nat, - }); - const Result_4 = IDL.Variant({ 'Ok' : Transaction, 'Err' : EkokeError }); const HttpRequest = IDL.Record({ 'url' : IDL.Text, 'method' : IDL.Text, @@ -171,7 +162,7 @@ export const idlFactory = ({ IDL }) => { 'created_at_time' : IDL.Opt(IDL.Nat64), 'amount' : IDL.Nat, }); - const Result_5 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : TransferError }); + const Result_4 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : TransferError }); const AllowanceArgs = IDL.Record({ 'account' : Account, 'spender' : Account, @@ -190,7 +181,7 @@ export const idlFactory = ({ IDL }) => { 'expires_at' : IDL.Opt(IDL.Nat64), 'spender' : Account, }); - const Result_6 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : ApproveError }); + const Result_5 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : ApproveError }); const TransferFromArgs = IDL.Record({ 'to' : Account, 'fee' : IDL.Opt(IDL.Nat), @@ -200,7 +191,7 @@ export const idlFactory = ({ IDL }) => { 'created_at_time' : IDL.Opt(IDL.Nat64), 'amount' : IDL.Nat, }); - const Result_7 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : TransferFromError }); + const Result_6 = IDL.Variant({ 'Ok' : IDL.Nat, 'Err' : TransferFromError }); const LiquidityPoolAccounts = IDL.Record({ 'icp' : Account, 'ckbtc' : Account, @@ -209,7 +200,7 @@ export const idlFactory = ({ IDL }) => { 'icp' : IDL.Nat, 'ckbtc' : IDL.Nat, }); - const Result_8 = IDL.Variant({ + const Result_7 = IDL.Variant({ 'Ok' : LiquidityPoolBalance, 'Err' : EkokeError, }); @@ -234,7 +225,6 @@ export const idlFactory = ({ IDL }) => { ), 'erc20_swap_fee' : IDL.Func([], [Result_2], []), 'get_contract_reward' : IDL.Func([IDL.Nat, IDL.Nat64], [Result_3], []), - 'get_transaction' : IDL.Func([IDL.Nat64], [Result_4], ['query']), 'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']), 'http_transform_send_tx' : IDL.Func( [TransformArgs], @@ -257,16 +247,16 @@ export const idlFactory = ({ IDL }) => { ), 'icrc1_symbol' : IDL.Func([], [IDL.Text], ['query']), 'icrc1_total_supply' : IDL.Func([], [IDL.Nat], ['query']), - 'icrc1_transfer' : IDL.Func([TransferArg], [Result_5], []), + 'icrc1_transfer' : IDL.Func([TransferArg], [Result_4], []), 'icrc2_allowance' : IDL.Func([AllowanceArgs], [Allowance], ['query']), - 'icrc2_approve' : IDL.Func([ApproveArgs], [Result_6], []), - 'icrc2_transfer_from' : IDL.Func([TransferFromArgs], [Result_7], []), + 'icrc2_approve' : IDL.Func([ApproveArgs], [Result_5], []), + 'icrc2_transfer_from' : IDL.Func([TransferFromArgs], [Result_6], []), 'liquidity_pool_accounts' : IDL.Func( [], [LiquidityPoolAccounts], ['query'], ), - 'liquidity_pool_balance' : IDL.Func([], [Result_8], ['query']), + 'liquidity_pool_balance' : IDL.Func([], [Result_7], ['query']), 'reserve_pool' : IDL.Func( [IDL.Nat, IDL.Nat, IDL.Opt(IDL.Vec(IDL.Nat8))], [Result_3], diff --git a/src/did/src/ekoke_index.rs b/src/did/src/ekoke_index.rs new file mode 100644 index 0000000..b9b10a7 --- /dev/null +++ b/src/did/src/ekoke_index.rs @@ -0,0 +1,40 @@ +mod transaction; + +use candid::{CandidType, Deserialize, Nat, Principal}; +use icrc::icrc1::account::{Account, Subaccount}; + +pub use self::transaction::{Approve, Burn, Mint, Transaction, TransactionWithId, Transfer}; + +pub type TxId = Nat; + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct EkokeIndexInitData { + /// ID of ekoke-ledger canister + pub ledger_id: Principal, +} + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct GetAccountTransactionArgs { + pub account: Account, + pub start: Option, + pub max_results: Nat, +} + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct GetTransactions { + pub transactions: Vec, + pub oldest_tx_id: Option, +} + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct GetTransactionsErr { + pub message: String, +} + +pub type GetTransactionsResult = Result; + +#[derive(Debug, Clone, CandidType, Deserialize)] +pub struct ListSubaccountsArgs { + pub owner: Principal, + pub start: Option, +} diff --git a/src/did/src/ekoke_index/transaction.rs b/src/did/src/ekoke_index/transaction.rs new file mode 100644 index 0000000..afca27b --- /dev/null +++ b/src/did/src/ekoke_index/transaction.rs @@ -0,0 +1,148 @@ +use candid::{CandidType, Decode, Deserialize, Encode, Nat}; +use ic_stable_structures::storable::Bound; +use ic_stable_structures::Storable; +use icrc::icrc1::account::Account; +use icrc::icrc1::transfer::Memo; + +use super::TxId; + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct TransactionWithId { + pub id: TxId, + pub transaction: Transaction, +} + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct Transaction { + pub kind: String, + pub mint: Option, + pub burn: Option, + pub transfer: Option, + pub approve: Option, + pub timestamp: u64, +} + +impl Storable for Transaction { + const BOUND: Bound = Bound::Bounded { + max_size: 4096, + is_fixed_size: false, + }; + + fn to_bytes(&self) -> std::borrow::Cow<[u8]> { + Encode!(&self).unwrap().into() + } + + fn from_bytes(bytes: std::borrow::Cow<[u8]>) -> Self { + Decode!(&bytes, Transaction).unwrap() + } +} + +impl Transaction { + pub fn from(&self) -> Option { + if let Some(burn) = &self.burn { + Some(burn.from) + } else if let Some(transfer) = &self.transfer { + Some(transfer.from) + } else { + self.approve.as_ref().map(|approve| approve.from) + } + } + + pub fn to(&self) -> Option { + if let Some(transfer) = &self.transfer { + Some(transfer.to) + } else { + self.mint.as_ref().map(|mint| mint.to) + } + } + + pub fn spender(&self) -> Option { + if let Some(transfer) = &self.transfer { + transfer.spender + } else if let Some(approve) = &self.approve { + approve.spender + } else if let Some(burn) = &self.burn { + burn.spender + } else { + None + } + } +} + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct Mint { + pub amount: Nat, + pub to: Account, + pub memo: Option, + pub created_at_time: Option, +} + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct Burn { + pub amount: Nat, + pub from: Account, + pub spender: Option, + pub memo: Option, + pub created_at_time: Option, +} + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct Transfer { + pub amount: Nat, + pub from: Account, + pub to: Account, + pub spender: Option, + pub memo: Option, + pub created_at_time: Option, + pub fee: Option, +} + +#[derive(Debug, Clone, PartialEq, CandidType, Deserialize)] +pub struct Approve { + pub amount: Nat, + pub from: Account, + pub spender: Option, + pub expected_allowance: Option, + pub expires_at: Option, + pub memo: Option, + pub created_at_time: Option, + pub fee: Option, +} + +#[cfg(test)] +mod test { + use candid::Principal; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_store_transaction_with_id() { + let tx = Transaction { + kind: "transfer".to_string(), + mint: None, + burn: None, + transfer: Some(Transfer { + amount: 100_u64.into(), + from: Account { + owner: Principal::management_canister(), + subaccount: Some([1u8; 32]), + }, + to: Account { + owner: Principal::management_canister(), + subaccount: None, + }, + spender: None, + memo: None, + created_at_time: None, + fee: None, + }), + approve: None, + timestamp: 0, + }; + + let data = tx.to_bytes(); + let decoded_tx = Transaction::from_bytes(data); + assert_eq!(tx, decoded_tx); + } +} diff --git a/src/did/src/lib.rs b/src/did/src/lib.rs index eede306..fddec16 100644 --- a/src/did/src/lib.rs +++ b/src/did/src/lib.rs @@ -6,6 +6,7 @@ mod common; pub mod deferred; pub mod ekoke; +pub mod ekoke_index; pub mod marketplace; pub use common::{ HttpApiRequest, HttpRequest, HttpResponse, StorableAccount, StorableNat, StorablePrincipal, diff --git a/src/ekoke_index/Cargo.toml b/src/ekoke_index/Cargo.toml new file mode 100644 index 0000000..96e48ea --- /dev/null +++ b/src/ekoke_index/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "ekoke_index" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[[bin]] +name = "ekoke-index-did" +path = "src/lib.rs" + +[lib] +crate-type = ["cdylib"] + +[features] +default = [] +did = [] + +[dependencies] +candid = { workspace = true } +did = { path = "../did" } +ic-cdk = { workspace = true } +ic-cdk-macros = { workspace = true } +ic-stable-structures = { workspace = true } +icrc = { path = "../icrc" } +num-bigint = { workspace = true } +num-traits = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +pretty_assertions = "1" +rand = "0.8.5" +tokio = { version = "1", features = ["full"] } diff --git a/src/ekoke_index/ekoke-index.did b/src/ekoke_index/ekoke-index.did new file mode 100644 index 0000000..441ce95 --- /dev/null +++ b/src/ekoke_index/ekoke-index.did @@ -0,0 +1,61 @@ +type Account = record { owner : principal; subaccount : opt vec nat8 }; +type Approve = record { + fee : opt nat; + from : Account; + memo : opt vec nat8; + created_at_time : opt nat64; + amount : nat; + expected_allowance : opt nat; + expires_at : opt nat64; + spender : opt Account; +}; +type Burn = record { + from : Account; + memo : opt vec nat8; + created_at_time : opt nat64; + amount : nat; + spender : opt Account; +}; +type EkokeIndexInitData = record { ledger_id : principal }; +type GetAccountTransactionArgs = record { + max_results : nat; + start : opt nat; + account : Account; +}; +type GetTransactions = record { + transactions : vec TransactionWithId; + oldest_tx_id : opt nat; +}; +type GetTransactionsErr = record { message : text }; +type ListSubaccountsArgs = record { owner : principal; start : opt vec nat8 }; +type Mint = record { + to : Account; + memo : opt vec nat8; + created_at_time : opt nat64; + amount : nat; +}; +type Result = variant { Ok : GetTransactions; Err : GetTransactionsErr }; +type Transaction = record { + burn : opt Burn; + kind : text; + mint : opt Mint; + approve : opt Approve; + timestamp : nat64; + transfer : opt Transfer; +}; +type TransactionWithId = record { id : nat; transaction : Transaction }; +type Transfer = record { + to : Account; + fee : opt nat; + from : Account; + memo : opt vec nat8; + created_at_time : opt nat64; + amount : nat; + spender : opt Account; +}; +service : (EkokeIndexInitData) -> { + commit : (Transaction) -> (nat); + get_account_transactions : (GetAccountTransactionArgs) -> (Result); + ledger_id : () -> (principal) query; + list_subaccounts : (ListSubaccountsArgs) -> (vec vec nat8) query; +} \ No newline at end of file diff --git a/src/ekoke_index/src/app.rs b/src/ekoke_index/src/app.rs new file mode 100644 index 0000000..4ab0d03 --- /dev/null +++ b/src/ekoke_index/src/app.rs @@ -0,0 +1,115 @@ +mod configuration; +mod index; +mod inspect; +mod memory; +#[cfg(test)] +mod test_utils; + +use candid::Principal; +use did::ekoke_index::{ + EkokeIndexInitData, GetAccountTransactionArgs, GetTransactionsResult, ListSubaccountsArgs, + Transaction, TxId, +}; +use icrc::icrc1::account::Subaccount; + +use self::configuration::Configuration; +use self::index::Index; +pub use self::inspect::Inspect; +use crate::utils::caller; + +pub struct EkokeIndexCanister; + +impl EkokeIndexCanister { + pub fn init(args: EkokeIndexInitData) { + Configuration::set_ledger_canister(args.ledger_id); + } + + pub fn post_upgrade() {} + + /// Get ledger canister id + pub fn ledger_id() -> Principal { + Configuration::get_ledger_canister() + } + + /// List subaccounts associated to a principal. + /// If start is provided, the list will start from the subaccount after the provided one. + pub fn list_subaccounts(args: ListSubaccountsArgs) -> Vec { + Index::list_subaccounts(args.owner, args.start) + } + + /// Get transactions for an account + pub fn get_account_transactions(args: GetAccountTransactionArgs) -> GetTransactionsResult { + Ok(Index::get_account_transactions( + args.account, + args.start, + args.max_results, + )) + } + + /// Commit a transaction into the Index + pub fn commit(tx: Transaction) -> TxId { + if !Inspect::inspect_is_ledger_canister(caller()) { + ic_cdk::trap("Unauthorized"); + } + + Index::commit(tx) + } +} + +#[cfg(test)] +mod test { + + use did::ekoke_index::Transfer; + use icrc::icrc1::account::Account; + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_init_canister() { + init_canister(); + assert_eq!(Configuration::get_ledger_canister(), caller()); + } + + #[test] + fn test_should_get_ledger_id() { + init_canister(); + assert_eq!(EkokeIndexCanister::ledger_id(), caller()); + } + + #[test] + fn test_should_commit_tx() { + init_canister(); + let tx = Transaction { + kind: "transfer".to_string(), + mint: None, + burn: None, + transfer: Some(Transfer { + amount: 100_u64.into(), + from: Account { + owner: Principal::management_canister(), + subaccount: Some([1u8; 32]), + }, + to: Account { + owner: Principal::management_canister(), + subaccount: None, + }, + spender: None, + memo: None, + created_at_time: None, + fee: None, + }), + approve: None, + timestamp: 0, + }; + assert_eq!(EkokeIndexCanister::commit(tx.clone()), 0u64); + assert_eq!(EkokeIndexCanister::commit(tx.clone()), 1u64); + } + + fn init_canister() { + let init_data = EkokeIndexInitData { + ledger_id: caller(), + }; + EkokeIndexCanister::init(init_data); + } +} diff --git a/src/ekoke_index/src/app/configuration.rs b/src/ekoke_index/src/app/configuration.rs new file mode 100644 index 0000000..dad5c8e --- /dev/null +++ b/src/ekoke_index/src/app/configuration.rs @@ -0,0 +1,56 @@ +//! # Configuration +//! +//! Canister configuration + +use std::cell::RefCell; + +use candid::Principal; +use did::StorablePrincipal; +use ic_stable_structures::memory_manager::VirtualMemory; +use ic_stable_structures::{DefaultMemoryImpl, StableCell}; + +use crate::app::memory::{LEDGER_CANISTER_ID_MEMORY_ID, MEMORY_MANAGER}; + +thread_local! { + /// Ledger canister + static LEDGER_CANISTER_ID: RefCell>> = + RefCell::new(StableCell::new(MEMORY_MANAGER.with(|mm| mm.get(LEDGER_CANISTER_ID_MEMORY_ID)), + Principal::anonymous().into()).unwrap() + ); +} + +/// canister configuration +pub struct Configuration; + +impl Configuration { + /// Set ledger canister id + pub fn set_ledger_canister(canister_id: Principal) { + LEDGER_CANISTER_ID.with_borrow_mut(|cell| { + cell.set(canister_id.into()).unwrap(); + }); + } + + /// Get ledger canister id + pub fn get_ledger_canister() -> Principal { + LEDGER_CANISTER_ID.with(|cell| cell.borrow().get().0) + } +} + +#[cfg(test)] +mod test { + + use std::str::FromStr as _; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_set_ledger_canister() { + let principal = + Principal::from_str("bs5l3-6b3zu-dpqyj-p2x4a-jyg4k-goneb-afof2-y5d62-skt67-3756q-dqe") + .unwrap(); + Configuration::set_ledger_canister(principal); + assert_eq!(Configuration::get_ledger_canister(), principal); + } +} diff --git a/src/ekoke_index/src/app/index.rs b/src/ekoke_index/src/app/index.rs new file mode 100644 index 0000000..a533c26 --- /dev/null +++ b/src/ekoke_index/src/app/index.rs @@ -0,0 +1,255 @@ +use std::cell::RefCell; +use std::collections::HashSet; + +use candid::{Nat, Principal}; +use did::ekoke_index::{GetTransactions, Transaction, TransactionWithId, TxId}; +use did::StorableAccount; +use ic_stable_structures::memory_manager::VirtualMemory; +use ic_stable_structures::{DefaultMemoryImpl, StableBTreeMap}; +use icrc::icrc1::account::{Account, Subaccount}; +use num_traits::ToPrimitive as _; + +use crate::app::memory::{ + ACCOUNTS_FROM_MEMORY_ID, ACCOUNTS_SPENDER_MEMORY_ID, ACCOUNTS_TO_MEMORY_ID, MEMORY_MANAGER, + TRANSACTIONS_MEMORY_ID, +}; + +thread_local! { + static TRANSACTIONS: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(TRANSACTIONS_MEMORY_ID)))); + + static TRANSACTIONS_FROM_ACCOUNTS: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(ACCOUNTS_FROM_MEMORY_ID))) + ); + + static TRANSACTIONS_TO_ACCOUNTS: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(ACCOUNTS_TO_MEMORY_ID))) + ); + + static TRANSACTIONS_SPENDER_ACCOUNTS: RefCell>> = + RefCell::new(StableBTreeMap::new(MEMORY_MANAGER.with(|mm| mm.get(ACCOUNTS_SPENDER_MEMORY_ID))) + ); +} + +/// The register contains the transactions history +pub struct Index; + +impl Index { + /// Insert a transaction in the index. + /// Also insert the accounts involved in the transaction + /// Returns the transaction ID + pub fn commit(tx: Transaction) -> TxId { + TRANSACTIONS.with_borrow_mut(|transactions| { + let id = transactions.len(); + // insert accounts + if let Some(from) = tx.from() { + TRANSACTIONS_FROM_ACCOUNTS.with_borrow_mut(|from_accounts| { + from_accounts.insert(id, from.into()); + }); + } + if let Some(to) = tx.to() { + TRANSACTIONS_TO_ACCOUNTS.with_borrow_mut(|to_accounts| { + to_accounts.insert(id, to.into()); + }); + } + if let Some(spender) = tx.spender() { + TRANSACTIONS_SPENDER_ACCOUNTS.with_borrow_mut(|spender_accounts| { + spender_accounts.insert(id, spender.into()); + }); + } + + // insert transaction + transactions.insert(id, tx); + + id.into() + }) + } + + /// List subaccounts associated to a principal. + /// If start is provided, the list will start from the subaccount after the provided one. + pub fn list_subaccounts(owner: Principal, start: Option) -> Vec { + let mut subaccounts = HashSet::new(); + // list all transactions with owner + let mut collect_accounts_fn = + |accounts: &StableBTreeMap>| { + for (_, account) in accounts.iter() { + if account.0.owner == owner { + if let Some(subaccount) = account.0.subaccount { + subaccounts.insert(subaccount); + } + } + } + }; + TRANSACTIONS_FROM_ACCOUNTS.with_borrow(&mut collect_accounts_fn); + TRANSACTIONS_TO_ACCOUNTS.with_borrow(&mut collect_accounts_fn); + TRANSACTIONS_SPENDER_ACCOUNTS.with_borrow(collect_accounts_fn); + + let mut subaccounts = subaccounts.into_iter().collect::>(); + if let Some(start) = start { + let index = subaccounts + .iter() + .position(|sa| *sa == start) + .unwrap_or_default(); + subaccounts = subaccounts.split_off(index + 1); + } + + subaccounts + } + + /// Get transactions for an account + pub fn get_account_transactions( + account: Account, + start: Option, + max_results: Nat, + ) -> GetTransactions { + let mut transactions = Vec::with_capacity(max_results.0.to_usize().unwrap_or_default()); + let start = start.map(|s| s.0.to_u64().unwrap_or_default()); + let mut oldest_tx_id = None; + // search for transactions + TRANSACTIONS.with_borrow(|transactions_map| { + for (id, tx) in transactions_map.iter() { + if tx.from() == Some(account) + || tx.to() == Some(account) + || tx.spender() == Some(account) + { + if let Some(start) = start { + if id < start { + continue; + } + } + if oldest_tx_id.is_none() { + oldest_tx_id = Some(id.into()); + } + transactions.push(TransactionWithId { + id: id.into(), + transaction: tx.clone(), + }); + if transactions.len() >= max_results.0.to_usize().unwrap_or_default() { + break; + } + } + } + }); + + GetTransactions { + oldest_tx_id, + transactions, + } + } +} + +#[cfg(test)] +mod test { + use did::ekoke_index::Transfer; + use pretty_assertions::assert_eq; + + use super::*; + use crate::app::test_utils::{ + alice, alice_account, bob, bob_account, charlie_account, random_alice_account, + }; + + #[test] + fn test_should_commit_transaction() { + let tx = Transaction { + kind: "transfer".to_string(), + mint: None, + burn: None, + transfer: Some(Transfer { + amount: 100_u64.into(), + from: alice_account(), + to: bob_account(), + spender: Some(charlie_account()), + memo: None, + created_at_time: None, + fee: None, + }), + approve: None, + timestamp: 0, + }; + + let tx_id = Index::commit(tx.clone()); + assert_eq!(tx_id, 0u64); + + // check tx + let spender = TRANSACTIONS_SPENDER_ACCOUNTS + .with_borrow(|spender_accounts| spender_accounts.get(&0u64).unwrap().0); + assert_eq!(spender, charlie_account()); + + let spender = TRANSACTIONS_TO_ACCOUNTS + .with_borrow(|spender_accounts| spender_accounts.get(&0u64).unwrap().0); + assert_eq!(spender, bob_account()); + + let spender = TRANSACTIONS_FROM_ACCOUNTS + .with_borrow(|spender_accounts| spender_accounts.get(&0u64).unwrap().0); + assert_eq!(spender, alice_account()); + } + + #[test] + fn test_should_get_subaccounts() { + for iter in 0..100u64 { + let tx = Transaction { + kind: "transfer".to_string(), + mint: None, + burn: None, + transfer: Some(Transfer { + amount: 100_u64.into(), + from: random_alice_account(), + to: bob_account(), + spender: None, + memo: None, + created_at_time: None, + fee: None, + }), + approve: None, + timestamp: 0, + }; + let tx_id = Index::commit(tx.clone()); + assert_eq!(tx_id, iter); + } + + // get subaccounts for alice + let subaccounts = Index::list_subaccounts(alice(), None); + assert_eq!(subaccounts.len(), 100); + + // get subaccounts for bob + let subaccounts = Index::list_subaccounts(bob(), None); + assert_eq!(subaccounts.len(), 1); + } + + #[test] + fn test_should_get_transactions() { + for iter in 0..100u64 { + let tx = Transaction { + kind: "transfer".to_string(), + mint: None, + burn: None, + transfer: Some(Transfer { + amount: 100_u64.into(), + from: random_alice_account(), + to: bob_account(), + spender: None, + memo: None, + created_at_time: None, + fee: None, + }), + approve: None, + timestamp: 0, + }; + let tx_id = Index::commit(tx.clone()); + assert_eq!(tx_id, iter); + } + + let account_txs = Index::get_account_transactions(bob_account(), None, 100u64.into()); + assert_eq!(account_txs.transactions.len(), 100); + assert_eq!(account_txs.oldest_tx_id, Some(0u64.into())); + + let account_txs = Index::get_account_transactions(bob_account(), None, 10_u64.into()); + assert_eq!(account_txs.transactions.len(), 10); + assert_eq!(account_txs.oldest_tx_id, Some(0u64.into())); + + let account_txs = + Index::get_account_transactions(bob_account(), Some(1u64.into()), 10_u64.into()); + assert_eq!(account_txs.transactions.len(), 10); + assert_eq!(account_txs.oldest_tx_id, Some(1u64.into())); + } +} diff --git a/src/ekoke_index/src/app/inspect.rs b/src/ekoke_index/src/app/inspect.rs new file mode 100644 index 0000000..20202bb --- /dev/null +++ b/src/ekoke_index/src/app/inspect.rs @@ -0,0 +1,34 @@ +use candid::Principal; + +use super::configuration::Configuration; + +pub struct Inspect; + +impl Inspect { + /// Inspect if the caller is the ledger canister + pub fn inspect_is_ledger_canister(caller: Principal) -> bool { + Configuration::get_ledger_canister() == caller + } +} + +#[cfg(test)] +mod test { + use std::str::FromStr as _; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_should_inspect_is_ledger_canister() { + let principal = Principal::from_str("fgzua-6iaaa-aaaaq-aacgq-cai").unwrap(); + + Configuration::set_ledger_canister(principal); + + assert_eq!( + Inspect::inspect_is_ledger_canister(Principal::anonymous()), + false + ); + assert_eq!(Inspect::inspect_is_ledger_canister(principal), true); + } +} diff --git a/src/ekoke_index/src/app/memory.rs b/src/ekoke_index/src/app/memory.rs new file mode 100644 index 0000000..cb04309 --- /dev/null +++ b/src/ekoke_index/src/app/memory.rs @@ -0,0 +1,15 @@ +use ic_stable_structures::memory_manager::{MemoryId, MemoryManager as IcMemoryManager}; +use ic_stable_structures::DefaultMemoryImpl; + +pub const TRANSACTIONS_MEMORY_ID: MemoryId = MemoryId::new(10); +pub const ACCOUNTS_FROM_MEMORY_ID: MemoryId = MemoryId::new(11); +pub const ACCOUNTS_TO_MEMORY_ID: MemoryId = MemoryId::new(12); +pub const ACCOUNTS_SPENDER_MEMORY_ID: MemoryId = MemoryId::new(13); + +// Configuration +pub const LEDGER_CANISTER_ID_MEMORY_ID: MemoryId = MemoryId::new(20); + +thread_local! { + /// Memory manager + pub static MEMORY_MANAGER: IcMemoryManager = IcMemoryManager::init(DefaultMemoryImpl::default()); +} diff --git a/src/ekoke_index/src/app/test_utils.rs b/src/ekoke_index/src/app/test_utils.rs new file mode 100644 index 0000000..ec39214 --- /dev/null +++ b/src/ekoke_index/src/app/test_utils.rs @@ -0,0 +1,58 @@ +use candid::Principal; +use icrc::icrc1::account::{Account, Subaccount, DEFAULT_SUBACCOUNT}; +use rand::Rng as _; + +pub fn alice() -> Principal { + Principal::from_text("be2us-64aaa-aaaaa-qaabq-cai").unwrap() +} + +pub fn alice_account() -> Account { + Account { + owner: alice(), + subaccount: Some(*DEFAULT_SUBACCOUNT), + } +} + +pub fn bob() -> Principal { + Principal::from_text("bs5l3-6b3zu-dpqyj-p2x4a-jyg4k-goneb-afof2-y5d62-skt67-3756q-dqe").unwrap() +} + +pub fn bob_account() -> Account { + Account { + owner: bob(), + subaccount: Some([ + 0x21, 0xa9, 0x95, 0x49, 0xe7, 0x92, 0x90, 0x7c, 0x5e, 0x27, 0x5e, 0x54, 0x51, 0x06, + 0x8d, 0x4d, 0xdf, 0x4d, 0x43, 0xee, 0x8d, 0xca, 0xb4, 0x87, 0x56, 0x23, 0x1a, 0x8f, + 0xb7, 0x71, 0x31, 0x23, + ]), + } +} + +pub fn charlie() -> Principal { + Principal::from_text("efv5g-kqaaa-aaaaq-aacaa-cai").unwrap() +} + +pub fn charlie_account() -> Account { + Account { + owner: charlie(), + subaccount: Some([ + 0x21, 0xa9, 0x95, 0x49, 0xe7, 0x92, 0x90, 0x7c, 0x5e, 0x27, 0x5e, 0x54, 0x51, 0x06, + 0x8d, 0x4d, 0xdf, 0x4d, 0x43, 0xee, 0x8d, 0xca, 0xb4, 0x87, 0x56, 0x23, 0x1a, 0x8f, + 0xb7, 0x71, 0x31, 0x23, + ]), + } +} + +pub fn random_alice_account() -> Account { + Account { + owner: alice(), + subaccount: Some(random_subaccounts()), + } +} + +fn random_subaccounts() -> Subaccount { + let mut rng = rand::thread_rng(); + let mut subaccount = [0u8; 32]; + rng.fill(&mut subaccount); + subaccount +} diff --git a/src/ekoke_index/src/inspect.rs b/src/ekoke_index/src/inspect.rs new file mode 100644 index 0000000..abf5564 --- /dev/null +++ b/src/ekoke_index/src/inspect.rs @@ -0,0 +1,31 @@ +use ic_cdk::api; +#[cfg(target_family = "wasm")] +use ic_cdk_macros::inspect_message; + +use crate::app::Inspect; +use crate::utils::caller; + +/// NOTE: inspect is disabled for non-wasm targets because without it we are getting a weird compilation error +/// in CI: +/// > multiple definition of `canister_inspect_message' +#[cfg(target_family = "wasm")] +#[inspect_message] +fn inspect_messages() { + inspect_message_impl() +} + +#[allow(dead_code)] +fn inspect_message_impl() { + let method = api::call::method_name(); + + let check_result = match method.as_str() { + "register_transaction" => Inspect::inspect_is_ledger_canister(caller()), + _ => true, + }; + + if check_result { + api::call::accept_message(); + } else { + ic_cdk::trap("Bad request"); + } +} diff --git a/src/ekoke_index/src/lib.rs b/src/ekoke_index/src/lib.rs new file mode 100644 index 0000000..1b4d307 --- /dev/null +++ b/src/ekoke_index/src/lib.rs @@ -0,0 +1,60 @@ +//! # Ekoke Index canister +//! +//! The ekoke index provides a map of which transactions are relevant for a given account + +mod app; +mod inspect; +mod utils; + +use candid::{candid_method, Principal}; +use did::ekoke_index::{ + EkokeIndexInitData, GetAccountTransactionArgs, GetTransactionsResult, ListSubaccountsArgs, + Transaction, TxId, +}; +use ic_cdk_macros::{init, post_upgrade, query, update}; +use icrc::icrc1::account::Subaccount; + +use self::app::EkokeIndexCanister; + +#[init] +pub fn init(data: EkokeIndexInitData) { + EkokeIndexCanister::init(data); +} + +#[post_upgrade] +pub fn post_upgrade() { + EkokeIndexCanister::post_upgrade(); +} + +#[query] +#[candid_method(query)] +pub fn ledger_id() -> Principal { + EkokeIndexCanister::ledger_id() +} + +#[query] +#[candid_method(query)] +pub fn list_subaccounts(args: ListSubaccountsArgs) -> Vec { + EkokeIndexCanister::list_subaccounts(args) +} + +#[update] +#[candid_method(update)] +pub fn get_account_transactions(args: GetAccountTransactionArgs) -> GetTransactionsResult { + EkokeIndexCanister::get_account_transactions(args) +} + +#[update] +#[candid_method(update)] +pub fn commit(tx: Transaction) -> TxId { + EkokeIndexCanister::commit(tx) +} + +#[allow(dead_code)] +fn main() { + // The line below generates did types and service definition from the + // methods annotated with `candid_method` above. The definition is then + // obtained with `__export_service()`. + candid::export_service!(); + std::print!("{}", __export_service()); +} diff --git a/src/ekoke_index/src/utils.rs b/src/ekoke_index/src/utils.rs new file mode 100644 index 0000000..2e85447 --- /dev/null +++ b/src/ekoke_index/src/utils.rs @@ -0,0 +1,13 @@ +use candid::Principal; + +pub fn caller() -> Principal { + #[cfg(not(target_arch = "wasm32"))] + { + Principal::from_text("zrrb4-gyxmq-nx67d-wmbky-k6xyt-byhmw-tr5ct-vsxu4-nuv2g-6rr65-aae") + .unwrap() + } + #[cfg(target_arch = "wasm32")] + { + ic_cdk::caller() + } +} diff --git a/src/ekoke_ledger/ekoke-ledger.did b/src/ekoke_ledger/ekoke-ledger.did index c87a50c..5332de5 100644 --- a/src/ekoke_ledger/ekoke-ledger.did +++ b/src/ekoke_ledger/ekoke-ledger.did @@ -111,21 +111,12 @@ type Result = variant { Ok; Err : EkokeError }; type Result_1 = variant { Ok : text; Err : EkokeError }; type Result_2 = variant { Ok : nat64; Err : EkokeError }; type Result_3 = variant { Ok : nat; Err : EkokeError }; -type Result_4 = variant { Ok : Transaction; Err : EkokeError }; -type Result_5 = variant { Ok : nat; Err : TransferError }; -type Result_6 = variant { Ok : nat; Err : ApproveError }; -type Result_7 = variant { Ok : nat; Err : TransferFromError }; -type Result_8 = variant { Ok : LiquidityPoolBalance; Err : EkokeError }; +type Result_4 = variant { Ok : nat; Err : TransferError }; +type Result_5 = variant { Ok : nat; Err : ApproveError }; +type Result_6 = variant { Ok : nat; Err : TransferFromError }; +type Result_7 = variant { Ok : LiquidityPoolBalance; Err : EkokeError }; type Role = variant { DeferredCanister; MarketplaceCanister; Admin }; type TokenExtension = record { url : text; name : text }; -type Transaction = record { - to : Account; - fee : nat; - from : Account; - memo : opt vec nat8; - created_at : nat64; - amount : nat; -}; type TransferArg = record { to : Account; fee : opt nat; @@ -182,7 +173,6 @@ service : (EkokeInitData) -> { erc20_swap : (text, nat, opt vec nat8) -> (Result_1); erc20_swap_fee : () -> (Result_2); get_contract_reward : (nat, nat64) -> (Result_3); - get_transaction : (nat64) -> (Result_4) query; http_request : (HttpRequest) -> (HttpResponse) query; http_transform_send_tx : (TransformArgs) -> (HttpResponse_1) query; icrc1_balance_of : (Account) -> (nat) query; @@ -193,12 +183,12 @@ service : (EkokeInitData) -> { icrc1_supported_standards : () -> (vec TokenExtension) query; icrc1_symbol : () -> (text) query; icrc1_total_supply : () -> (nat) query; - icrc1_transfer : (TransferArg) -> (Result_5); + icrc1_transfer : (TransferArg) -> (Result_4); icrc2_allowance : (AllowanceArgs) -> (Allowance) query; - icrc2_approve : (ApproveArgs) -> (Result_6); - icrc2_transfer_from : (TransferFromArgs) -> (Result_7); + icrc2_approve : (ApproveArgs) -> (Result_5); + icrc2_transfer_from : (TransferFromArgs) -> (Result_6); liquidity_pool_accounts : () -> (LiquidityPoolAccounts) query; - liquidity_pool_balance : () -> (Result_8) query; + liquidity_pool_balance : () -> (Result_7) query; reserve_pool : (nat, nat, opt vec nat8) -> (Result_3); send_reward : (nat, nat, Account) -> (Result); } \ No newline at end of file diff --git a/src/ekoke_ledger/src/app.rs b/src/ekoke_ledger/src/app.rs index 230dd77..602c99d 100644 --- a/src/ekoke_ledger/src/app.rs +++ b/src/ekoke_ledger/src/app.rs @@ -126,11 +126,6 @@ impl EkokeCanister { ); } - /// Get transaction by id - pub fn get_transaction(id: u64) -> EkokeResult { - Register::get_tx(id) - } - /// Reserve a pool for the provided contract ID with the provided amount of $picoEkoke tokens. /// /// The tokens are withdrawned from the from's wallet. @@ -671,29 +666,6 @@ mod test { assert_eq!(Configuration::get_eth_network(), EthNetwork::Goerli); } - #[tokio::test] - async fn test_should_get_transaction() { - init_canister(); - let now = utils::time(); - assert!(Register::insert_tx(Transaction { - from: caller_account(), - to: bob_account(), - amount: ekoke_to_picoekoke(10_000), - fee: Nat::from(ICRC1_FEE), - memo: None, - created_at: now, - }) - .is_ok()); - - let tx = EkokeCanister::get_transaction(0).unwrap(); - assert_eq!(tx.from, caller_account()); - assert_eq!(tx.to, bob_account()); - assert_eq!(tx.amount, ekoke_to_picoekoke(10_000)); - assert_eq!(tx.fee, Nat::from(ICRC1_FEE)); - assert_eq!(tx.memo, None); - assert_eq!(tx.created_at, now); - } - #[tokio::test] async fn test_should_reserve_pool() { init_canister(); diff --git a/src/ekoke_ledger/src/app/register.rs b/src/ekoke_ledger/src/app/register.rs index 847792d..cf29ce8 100644 --- a/src/ekoke_ledger/src/app/register.rs +++ b/src/ekoke_ledger/src/app/register.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use candid::Nat; -use did::ekoke::{EkokeError, EkokeResult, RegisterError, Transaction}; +use did::ekoke::{EkokeError, EkokeResult, Transaction}; use ic_stable_structures::memory_manager::VirtualMemory; use ic_stable_structures::{DefaultMemoryImpl, StableVec}; @@ -26,53 +26,4 @@ impl Register { Ok(id.into()) }) } - - /// Get a transaction from the register by its ID - pub fn get_tx(id: u64) -> EkokeResult { - REGISTER.with_borrow(|register| { - register - .get(id) - .ok_or(EkokeError::Register(RegisterError::TransactionNotFound)) - }) - } -} - -#[cfg(test)] -mod test { - - use icrc::icrc1::transfer::Memo; - use pretty_assertions::assert_eq; - - use super::*; - use crate::app::test_utils::{alice_account, bob_account, ekoke_to_picoekoke}; - use crate::constants::ICRC1_FEE; - - #[test] - fn test_should_insert_tx() { - let tx = Transaction { - from: alice_account(), - to: bob_account(), - amount: ekoke_to_picoekoke(50), - fee: ICRC1_FEE.into(), - memo: None, - created_at: crate::utils::time(), - }; - assert_eq!(Register::insert_tx(tx).unwrap(), Nat::from(0_u64)); - assert!(Register::get_tx(0).is_ok()); - - let tx = Transaction { - from: alice_account(), - to: bob_account(), - amount: ekoke_to_picoekoke(50), - fee: ICRC1_FEE.into(), - memo: Some(Memo::from( - "12341235412523524353451234123541".as_bytes().to_vec(), - )), - created_at: crate::utils::time(), - }; - assert_eq!(Register::insert_tx(tx).unwrap(), Nat::from(1_u64)); - assert!(Register::get_tx(1).is_ok()); - - assert!(Register::get_tx(2).is_err()); - } } diff --git a/src/ekoke_ledger/src/lib.rs b/src/ekoke_ledger/src/lib.rs index db3ff8c..dc2dbc7 100644 --- a/src/ekoke_ledger/src/lib.rs +++ b/src/ekoke_ledger/src/lib.rs @@ -1,4 +1,4 @@ -//! # Ekoke +//! # Ekoke Ledger canister //! //! The ekoke canister serves a ICRC-2 token called $EKOKE, which is the reward token for Deferred transactions. //! It is a deflationary token which ... @@ -13,7 +13,6 @@ mod utils; use candid::{candid_method, Nat, Principal}; use did::ekoke::{ EkokeInitData, EkokeResult, LiquidityPoolAccounts, LiquidityPoolBalance, PicoEkoke, Role, - Transaction, }; use did::{H160, ID}; use ic_cdk::api::management_canister::http_request::{HttpResponse, TransformArgs}; @@ -164,12 +163,6 @@ pub async fn admin_eth_wallet_address() -> H160 { EkokeCanister::admin_eth_wallet_address().await } -#[query] -#[candid_method(query)] -pub fn get_transaction(id: u64) -> EkokeResult { - EkokeCanister::get_transaction(id) -} - // icrc-1 #[query]