From 9b167c05eaf31c3bcea7989350b410b4aeb69aa4 Mon Sep 17 00:00:00 2001 From: veeso Date: Sun, 25 Feb 2024 15:41:52 +0100 Subject: [PATCH] fix: swap erc20 into icrc --- ethereum/ekoke/README.md | 2 +- .../ekoke-erc20-swap-frontend.did | 262 ++++++++++++++++ .../ekoke-erc20-swap-frontend.did.d.ts | 240 +++++++++++++++ .../ekoke-erc20-swap-frontend.did.js | 286 ++++++++++++++++++ .../ekoke-erc20-swap-frontend/index.d.ts | 50 +++ .../ekoke-erc20-swap-frontend/index.js | 43 +++ src/ekoke_erc20_swap_frontend/package.json | 2 + .../src/js/components/App/pages/Summary.tsx | 12 +- .../components/App/pages/SwapErc20ToIcrc.tsx | 192 ++++++++++++ .../src/js/components/AppLayout.tsx | 2 +- .../src/js/components/Header.tsx | 13 + .../src/js/components/Header/Desktop.tsx | 32 ++ .../src/js/components/Header/Mobile.tsx | 15 + .../src/js/components/TaskList/TaskEntry.tsx | 36 +++ .../src/js/components/TaskList/TaskList.tsx | 122 ++++++++ .../src/js/components/reusable/Container.tsx | 2 +- .../src/js/components/reusable/Dialog.tsx | 29 -- .../src/js/components/svg/Ethereum.tsx | 33 ++ .../src/js/components/svg/EthereumWhite.tsx | 33 ++ .../js/components/svg/InternetComputer.tsx | 78 +++++ .../src/js/components/svg/Metamask.tsx | 94 ++++++ src/ekoke_erc20_swap_frontend/src/js/utils.ts | 29 ++ .../src/js/web3/Web3Client.ts | 37 +-- .../tailwind.config.ts | 1 + src/ekoke_erc20_swap_frontend/yarn.lock | 4 +- 25 files changed, 1577 insertions(+), 72 deletions(-) create mode 100644 src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did create mode 100644 src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.d.ts create mode 100644 src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.js create mode 100644 src/declarations/ekoke-erc20-swap-frontend/index.d.ts create mode 100644 src/declarations/ekoke-erc20-swap-frontend/index.js create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/App/pages/SwapErc20ToIcrc.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/Header.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/Header/Desktop.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/Header/Mobile.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/TaskList/TaskEntry.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/TaskList/TaskList.tsx delete mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/reusable/Dialog.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/svg/Ethereum.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/svg/EthereumWhite.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/svg/InternetComputer.tsx create mode 100644 src/ekoke_erc20_swap_frontend/src/js/components/svg/Metamask.tsx diff --git a/ethereum/ekoke/README.md b/ethereum/ekoke/README.md index dddd454..0f5fa31 100644 --- a/ethereum/ekoke/README.md +++ b/ethereum/ekoke/README.md @@ -12,4 +12,4 @@ yarn hardhat verify --network goerli "$CONTRACT_ADDRESS" "$OWNER_ADDRESS" $SWAP_ ## Address -- Sepolia ERC20 Bridge: [0xc08e14F47382BCc1dA6c3Ff366018cAb1c77091F](https://sepolia.etherscan.io/address/0xc08e14F47382BCc1dA6c3Ff366018cAb1c77091F) +- Sepolia: [0x30eBEE43A1f7Ba89C78Eb4Adde3ada425DAA473d](https://sepolia.etherscan.io/address/0x30eBEE43A1f7Ba89C78Eb4Adde3ada425DAA473d) diff --git a/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did new file mode 100644 index 0000000..51bb1a2 --- /dev/null +++ b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did @@ -0,0 +1,262 @@ +type BatchId = nat; +type ChunkId = nat; +type Key = text; +type Time = int; + +type CreateAssetArguments = record { + key: Key; + content_type: text; + max_age: opt nat64; + headers: opt vec HeaderField; + enable_aliasing: opt bool; + allow_raw_access: opt bool; +}; + +// Add or change content for an asset, by content encoding +type SetAssetContentArguments = record { + key: Key; + content_encoding: text; + chunk_ids: vec ChunkId; + sha256: opt blob; +}; + +// Remove content for an asset, by content encoding +type UnsetAssetContentArguments = record { + key: Key; + content_encoding: text; +}; + +// Delete an asset +type DeleteAssetArguments = record { + key: Key; +}; + +// Reset everything +type ClearArguments = record {}; + +type BatchOperationKind = variant { + CreateAsset: CreateAssetArguments; + SetAssetContent: SetAssetContentArguments; + + SetAssetProperties: SetAssetPropertiesArguments; + + UnsetAssetContent: UnsetAssetContentArguments; + DeleteAsset: DeleteAssetArguments; + + Clear: ClearArguments; +}; + +type CommitBatchArguments = record { + batch_id: BatchId; + operations: vec BatchOperationKind +}; + +type CommitProposedBatchArguments = record { + batch_id: BatchId; + evidence: blob; +}; + +type ComputeEvidenceArguments = record { + batch_id: BatchId; + max_iterations: opt nat16 +}; + +type DeleteBatchArguments = record { + batch_id: BatchId; +}; + +type HeaderField = record { text; text; }; + +type HttpRequest = record { + method: text; + url: text; + headers: vec HeaderField; + body: blob; + certificate_version: opt nat16; +}; + +type HttpResponse = record { + status_code: nat16; + headers: vec HeaderField; + body: blob; + streaming_strategy: opt StreamingStrategy; +}; + +type StreamingCallbackHttpResponse = record { + body: blob; + token: opt StreamingCallbackToken; +}; + +type StreamingCallbackToken = record { + key: Key; + content_encoding: text; + index: nat; + sha256: opt blob; +}; + +type StreamingStrategy = variant { + Callback: record { + callback: func (StreamingCallbackToken) -> (opt StreamingCallbackHttpResponse) query; + token: StreamingCallbackToken; + }; +}; + +type SetAssetPropertiesArguments = record { + key: Key; + max_age: opt opt nat64; + headers: opt opt vec HeaderField; + allow_raw_access: opt opt bool; + is_aliased: opt opt bool; +}; + +type ConfigurationResponse = record { + max_batches: opt nat64; + max_chunks: opt nat64; + max_bytes: opt nat64; +}; + +type ConfigureArguments = record { + max_batches: opt opt nat64; + max_chunks: opt opt nat64; + max_bytes: opt opt nat64; +}; + +type Permission = variant { + Commit; + ManagePermissions; + Prepare; +}; + +type GrantPermission = record { + to_principal: principal; + permission: Permission; +}; +type RevokePermission = record { + of_principal: principal; + permission: Permission; +}; +type ListPermitted = record { permission: Permission }; + +type ValidationResult = variant { Ok : text; Err : text }; + +type AssetCanisterArgs = variant { + Init: InitArgs; + Upgrade: UpgradeArgs; +}; + +type InitArgs = record {}; + +type UpgradeArgs = record { + set_permissions: opt SetPermissions; +}; + +/// Sets the list of principals granted each permission. +type SetPermissions = record { + prepare: vec principal; + commit: vec principal; + manage_permissions: vec principal; +}; + +service: (asset_canister_args: opt AssetCanisterArgs) -> { + api_version: () -> (nat16) query; + + get: (record { + key: Key; + accept_encodings: vec text; + }) -> (record { + content: blob; // may be the entirety of the content, or just chunk index 0 + content_type: text; + content_encoding: text; + sha256: opt blob; // sha256 of entire asset encoding, calculated by dfx and passed in SetAssetContentArguments + total_length: nat; // all chunks except last have size == content.size() + }) query; + + // if get() returned chunks > 1, call this to retrieve them. + // chunks may or may not be split up at the same boundaries as presented to create_chunk(). + get_chunk: (record { + key: Key; + content_encoding: text; + index: nat; + sha256: opt blob; // sha256 of entire asset encoding, calculated by dfx and passed in SetAssetContentArguments + }) -> (record { content: blob }) query; + + list : (record {}) -> (vec record { + key: Key; + content_type: text; + encodings: vec record { + content_encoding: text; + sha256: opt blob; // sha256 of entire asset encoding, calculated by dfx and passed in SetAssetContentArguments + length: nat; // Size of this encoding's blob. Calculated when uploading assets. + modified: Time; + }; + }) query; + + certified_tree : (record {}) -> (record { + certificate: blob; + tree: blob; + }) query; + + create_batch : (record {}) -> (record { batch_id: BatchId }); + + create_chunk: (record { batch_id: BatchId; content: blob }) -> (record { chunk_id: ChunkId }); + + // Perform all operations successfully, or reject + commit_batch: (CommitBatchArguments) -> (); + + // Save the batch operations for later commit + propose_commit_batch: (CommitBatchArguments) -> (); + + // Given a batch already proposed, perform all operations successfully, or reject + commit_proposed_batch: (CommitProposedBatchArguments) -> (); + + // Compute a hash over the CommitBatchArguments. Call until it returns Some(evidence). + compute_evidence: (ComputeEvidenceArguments) -> (opt blob); + + // Delete a batch that has been created, or proposed for commit, but not yet committed + delete_batch: (DeleteBatchArguments) -> (); + + create_asset: (CreateAssetArguments) -> (); + set_asset_content: (SetAssetContentArguments) -> (); + unset_asset_content: (UnsetAssetContentArguments) -> (); + + delete_asset: (DeleteAssetArguments) -> (); + + clear: (ClearArguments) -> (); + + // Single call to create an asset with content for a single content encoding that + // fits within the message ingress limit. + store: (record { + key: Key; + content_type: text; + content_encoding: text; + content: blob; + sha256: opt blob + }) -> (); + + http_request: (request: HttpRequest) -> (HttpResponse) query; + http_request_streaming_callback: (token: StreamingCallbackToken) -> (opt StreamingCallbackHttpResponse) query; + + authorize: (principal) -> (); + deauthorize: (principal) -> (); + list_authorized: () -> (vec principal); + grant_permission: (GrantPermission) -> (); + revoke_permission: (RevokePermission) -> (); + list_permitted: (ListPermitted) -> (vec principal); + take_ownership: () -> (); + + get_asset_properties : (key: Key) -> (record { + max_age: opt nat64; + headers: opt vec HeaderField; + allow_raw_access: opt bool; + is_aliased: opt bool; } ) query; + set_asset_properties: (SetAssetPropertiesArguments) -> (); + + get_configuration: () -> (ConfigurationResponse); + configure: (ConfigureArguments) -> (); + + validate_grant_permission: (GrantPermission) -> (ValidationResult); + validate_revoke_permission: (RevokePermission) -> (ValidationResult); + validate_take_ownership: () -> (ValidationResult); + validate_commit_proposed_batch: (CommitProposedBatchArguments) -> (ValidationResult); + validate_configure: (ConfigureArguments) -> (ValidationResult); +} diff --git a/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.d.ts b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.d.ts new file mode 100644 index 0000000..0b4168e --- /dev/null +++ b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.d.ts @@ -0,0 +1,240 @@ +import type { Principal } from '@dfinity/principal'; +import type { ActorMethod } from '@dfinity/agent'; +import type { IDL } from '@dfinity/candid'; + +export type AssetCanisterArgs = { 'Upgrade' : UpgradeArgs } | + { 'Init' : InitArgs }; +export type BatchId = bigint; +export type BatchOperationKind = { + 'SetAssetProperties' : SetAssetPropertiesArguments + } | + { 'CreateAsset' : CreateAssetArguments } | + { 'UnsetAssetContent' : UnsetAssetContentArguments } | + { 'DeleteAsset' : DeleteAssetArguments } | + { 'SetAssetContent' : SetAssetContentArguments } | + { 'Clear' : ClearArguments }; +export type ChunkId = bigint; +export type ClearArguments = {}; +export interface CommitBatchArguments { + 'batch_id' : BatchId, + 'operations' : Array, +} +export interface CommitProposedBatchArguments { + 'batch_id' : BatchId, + 'evidence' : Uint8Array | number[], +} +export interface ComputeEvidenceArguments { + 'batch_id' : BatchId, + 'max_iterations' : [] | [number], +} +export interface ConfigurationResponse { + 'max_batches' : [] | [bigint], + 'max_bytes' : [] | [bigint], + 'max_chunks' : [] | [bigint], +} +export interface ConfigureArguments { + 'max_batches' : [] | [[] | [bigint]], + 'max_bytes' : [] | [[] | [bigint]], + 'max_chunks' : [] | [[] | [bigint]], +} +export interface CreateAssetArguments { + 'key' : Key, + 'content_type' : string, + 'headers' : [] | [Array], + 'allow_raw_access' : [] | [boolean], + 'max_age' : [] | [bigint], + 'enable_aliasing' : [] | [boolean], +} +export interface DeleteAssetArguments { 'key' : Key } +export interface DeleteBatchArguments { 'batch_id' : BatchId } +export interface GrantPermission { + 'permission' : Permission, + 'to_principal' : Principal, +} +export type HeaderField = [string, string]; +export interface HttpRequest { + 'url' : string, + 'method' : string, + 'body' : Uint8Array | number[], + 'headers' : Array, + 'certificate_version' : [] | [number], +} +export interface HttpResponse { + 'body' : Uint8Array | number[], + 'headers' : Array, + 'streaming_strategy' : [] | [StreamingStrategy], + 'status_code' : number, +} +export type InitArgs = {}; +export type Key = string; +export interface ListPermitted { 'permission' : Permission } +export type Permission = { 'Prepare' : null } | + { 'ManagePermissions' : null } | + { 'Commit' : null }; +export interface RevokePermission { + 'permission' : Permission, + 'of_principal' : Principal, +} +export interface SetAssetContentArguments { + 'key' : Key, + 'sha256' : [] | [Uint8Array | number[]], + 'chunk_ids' : Array, + 'content_encoding' : string, +} +export interface SetAssetPropertiesArguments { + 'key' : Key, + 'headers' : [] | [[] | [Array]], + 'is_aliased' : [] | [[] | [boolean]], + 'allow_raw_access' : [] | [[] | [boolean]], + 'max_age' : [] | [[] | [bigint]], +} +export interface SetPermissions { + 'prepare' : Array, + 'commit' : Array, + 'manage_permissions' : Array, +} +export interface StreamingCallbackHttpResponse { + 'token' : [] | [StreamingCallbackToken], + 'body' : Uint8Array | number[], +} +export interface StreamingCallbackToken { + 'key' : Key, + 'sha256' : [] | [Uint8Array | number[]], + 'index' : bigint, + 'content_encoding' : string, +} +export type StreamingStrategy = { + 'Callback' : { + 'token' : StreamingCallbackToken, + 'callback' : [Principal, string], + } + }; +export type Time = bigint; +export interface UnsetAssetContentArguments { + 'key' : Key, + 'content_encoding' : string, +} +export interface UpgradeArgs { 'set_permissions' : [] | [SetPermissions] } +export type ValidationResult = { 'Ok' : string } | + { 'Err' : string }; +export interface _SERVICE { + 'api_version' : ActorMethod<[], number>, + 'authorize' : ActorMethod<[Principal], undefined>, + 'certified_tree' : ActorMethod< + [{}], + { 'certificate' : Uint8Array | number[], 'tree' : Uint8Array | number[] } + >, + 'clear' : ActorMethod<[ClearArguments], undefined>, + 'commit_batch' : ActorMethod<[CommitBatchArguments], undefined>, + 'commit_proposed_batch' : ActorMethod< + [CommitProposedBatchArguments], + undefined + >, + 'compute_evidence' : ActorMethod< + [ComputeEvidenceArguments], + [] | [Uint8Array | number[]] + >, + 'configure' : ActorMethod<[ConfigureArguments], undefined>, + 'create_asset' : ActorMethod<[CreateAssetArguments], undefined>, + 'create_batch' : ActorMethod<[{}], { 'batch_id' : BatchId }>, + 'create_chunk' : ActorMethod< + [{ 'content' : Uint8Array | number[], 'batch_id' : BatchId }], + { 'chunk_id' : ChunkId } + >, + 'deauthorize' : ActorMethod<[Principal], undefined>, + 'delete_asset' : ActorMethod<[DeleteAssetArguments], undefined>, + 'delete_batch' : ActorMethod<[DeleteBatchArguments], undefined>, + 'get' : ActorMethod< + [{ 'key' : Key, 'accept_encodings' : Array }], + { + 'content' : Uint8Array | number[], + 'sha256' : [] | [Uint8Array | number[]], + 'content_type' : string, + 'content_encoding' : string, + 'total_length' : bigint, + } + >, + 'get_asset_properties' : ActorMethod< + [Key], + { + 'headers' : [] | [Array], + 'is_aliased' : [] | [boolean], + 'allow_raw_access' : [] | [boolean], + 'max_age' : [] | [bigint], + } + >, + 'get_chunk' : ActorMethod< + [ + { + 'key' : Key, + 'sha256' : [] | [Uint8Array | number[]], + 'index' : bigint, + 'content_encoding' : string, + }, + ], + { 'content' : Uint8Array | number[] } + >, + 'get_configuration' : ActorMethod<[], ConfigurationResponse>, + 'grant_permission' : ActorMethod<[GrantPermission], undefined>, + 'http_request' : ActorMethod<[HttpRequest], HttpResponse>, + 'http_request_streaming_callback' : ActorMethod< + [StreamingCallbackToken], + [] | [StreamingCallbackHttpResponse] + >, + 'list' : ActorMethod< + [{}], + Array< + { + 'key' : Key, + 'encodings' : Array< + { + 'modified' : Time, + 'sha256' : [] | [Uint8Array | number[]], + 'length' : bigint, + 'content_encoding' : string, + } + >, + 'content_type' : string, + } + > + >, + 'list_authorized' : ActorMethod<[], Array>, + 'list_permitted' : ActorMethod<[ListPermitted], Array>, + 'propose_commit_batch' : ActorMethod<[CommitBatchArguments], undefined>, + 'revoke_permission' : ActorMethod<[RevokePermission], undefined>, + 'set_asset_content' : ActorMethod<[SetAssetContentArguments], undefined>, + 'set_asset_properties' : ActorMethod< + [SetAssetPropertiesArguments], + undefined + >, + 'store' : ActorMethod< + [ + { + 'key' : Key, + 'content' : Uint8Array | number[], + 'sha256' : [] | [Uint8Array | number[]], + 'content_type' : string, + 'content_encoding' : string, + }, + ], + undefined + >, + 'take_ownership' : ActorMethod<[], undefined>, + 'unset_asset_content' : ActorMethod<[UnsetAssetContentArguments], undefined>, + 'validate_commit_proposed_batch' : ActorMethod< + [CommitProposedBatchArguments], + ValidationResult + >, + 'validate_configure' : ActorMethod<[ConfigureArguments], ValidationResult>, + 'validate_grant_permission' : ActorMethod< + [GrantPermission], + ValidationResult + >, + 'validate_revoke_permission' : ActorMethod< + [RevokePermission], + ValidationResult + >, + 'validate_take_ownership' : ActorMethod<[], ValidationResult>, +} +export declare const idlFactory: IDL.InterfaceFactory; +export declare const init: ({ IDL }: { IDL: IDL }) => IDL.Type[]; diff --git a/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.js b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.js new file mode 100644 index 0000000..b68ea4a --- /dev/null +++ b/src/declarations/ekoke-erc20-swap-frontend/ekoke-erc20-swap-frontend.did.js @@ -0,0 +1,286 @@ +export const idlFactory = ({ IDL }) => { + const SetPermissions = IDL.Record({ + 'prepare' : IDL.Vec(IDL.Principal), + 'commit' : IDL.Vec(IDL.Principal), + 'manage_permissions' : IDL.Vec(IDL.Principal), + }); + const UpgradeArgs = IDL.Record({ + 'set_permissions' : IDL.Opt(SetPermissions), + }); + const InitArgs = IDL.Record({}); + const AssetCanisterArgs = IDL.Variant({ + 'Upgrade' : UpgradeArgs, + 'Init' : InitArgs, + }); + const ClearArguments = IDL.Record({}); + const BatchId = IDL.Nat; + const Key = IDL.Text; + const HeaderField = IDL.Tuple(IDL.Text, IDL.Text); + const SetAssetPropertiesArguments = IDL.Record({ + 'key' : Key, + 'headers' : IDL.Opt(IDL.Opt(IDL.Vec(HeaderField))), + 'is_aliased' : IDL.Opt(IDL.Opt(IDL.Bool)), + 'allow_raw_access' : IDL.Opt(IDL.Opt(IDL.Bool)), + 'max_age' : IDL.Opt(IDL.Opt(IDL.Nat64)), + }); + const CreateAssetArguments = IDL.Record({ + 'key' : Key, + 'content_type' : IDL.Text, + 'headers' : IDL.Opt(IDL.Vec(HeaderField)), + 'allow_raw_access' : IDL.Opt(IDL.Bool), + 'max_age' : IDL.Opt(IDL.Nat64), + 'enable_aliasing' : IDL.Opt(IDL.Bool), + }); + const UnsetAssetContentArguments = IDL.Record({ + 'key' : Key, + 'content_encoding' : IDL.Text, + }); + const DeleteAssetArguments = IDL.Record({ 'key' : Key }); + const ChunkId = IDL.Nat; + const SetAssetContentArguments = IDL.Record({ + 'key' : Key, + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'chunk_ids' : IDL.Vec(ChunkId), + 'content_encoding' : IDL.Text, + }); + const BatchOperationKind = IDL.Variant({ + 'SetAssetProperties' : SetAssetPropertiesArguments, + 'CreateAsset' : CreateAssetArguments, + 'UnsetAssetContent' : UnsetAssetContentArguments, + 'DeleteAsset' : DeleteAssetArguments, + 'SetAssetContent' : SetAssetContentArguments, + 'Clear' : ClearArguments, + }); + const CommitBatchArguments = IDL.Record({ + 'batch_id' : BatchId, + 'operations' : IDL.Vec(BatchOperationKind), + }); + const CommitProposedBatchArguments = IDL.Record({ + 'batch_id' : BatchId, + 'evidence' : IDL.Vec(IDL.Nat8), + }); + const ComputeEvidenceArguments = IDL.Record({ + 'batch_id' : BatchId, + 'max_iterations' : IDL.Opt(IDL.Nat16), + }); + const ConfigureArguments = IDL.Record({ + 'max_batches' : IDL.Opt(IDL.Opt(IDL.Nat64)), + 'max_bytes' : IDL.Opt(IDL.Opt(IDL.Nat64)), + 'max_chunks' : IDL.Opt(IDL.Opt(IDL.Nat64)), + }); + const DeleteBatchArguments = IDL.Record({ 'batch_id' : BatchId }); + const ConfigurationResponse = IDL.Record({ + 'max_batches' : IDL.Opt(IDL.Nat64), + 'max_bytes' : IDL.Opt(IDL.Nat64), + 'max_chunks' : IDL.Opt(IDL.Nat64), + }); + const Permission = IDL.Variant({ + 'Prepare' : IDL.Null, + 'ManagePermissions' : IDL.Null, + 'Commit' : IDL.Null, + }); + const GrantPermission = IDL.Record({ + 'permission' : Permission, + 'to_principal' : IDL.Principal, + }); + const HttpRequest = IDL.Record({ + 'url' : IDL.Text, + 'method' : IDL.Text, + 'body' : IDL.Vec(IDL.Nat8), + 'headers' : IDL.Vec(HeaderField), + 'certificate_version' : IDL.Opt(IDL.Nat16), + }); + const StreamingCallbackToken = IDL.Record({ + 'key' : Key, + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'index' : IDL.Nat, + 'content_encoding' : IDL.Text, + }); + const StreamingCallbackHttpResponse = IDL.Record({ + 'token' : IDL.Opt(StreamingCallbackToken), + 'body' : IDL.Vec(IDL.Nat8), + }); + const StreamingStrategy = IDL.Variant({ + 'Callback' : IDL.Record({ + 'token' : StreamingCallbackToken, + 'callback' : IDL.Func( + [StreamingCallbackToken], + [IDL.Opt(StreamingCallbackHttpResponse)], + ['query'], + ), + }), + }); + const HttpResponse = IDL.Record({ + 'body' : IDL.Vec(IDL.Nat8), + 'headers' : IDL.Vec(HeaderField), + 'streaming_strategy' : IDL.Opt(StreamingStrategy), + 'status_code' : IDL.Nat16, + }); + const Time = IDL.Int; + const ListPermitted = IDL.Record({ 'permission' : Permission }); + const RevokePermission = IDL.Record({ + 'permission' : Permission, + 'of_principal' : IDL.Principal, + }); + const ValidationResult = IDL.Variant({ 'Ok' : IDL.Text, 'Err' : IDL.Text }); + return IDL.Service({ + 'api_version' : IDL.Func([], [IDL.Nat16], ['query']), + 'authorize' : IDL.Func([IDL.Principal], [], []), + 'certified_tree' : IDL.Func( + [IDL.Record({})], + [ + IDL.Record({ + 'certificate' : IDL.Vec(IDL.Nat8), + 'tree' : IDL.Vec(IDL.Nat8), + }), + ], + ['query'], + ), + 'clear' : IDL.Func([ClearArguments], [], []), + 'commit_batch' : IDL.Func([CommitBatchArguments], [], []), + 'commit_proposed_batch' : IDL.Func([CommitProposedBatchArguments], [], []), + 'compute_evidence' : IDL.Func( + [ComputeEvidenceArguments], + [IDL.Opt(IDL.Vec(IDL.Nat8))], + [], + ), + 'configure' : IDL.Func([ConfigureArguments], [], []), + 'create_asset' : IDL.Func([CreateAssetArguments], [], []), + 'create_batch' : IDL.Func( + [IDL.Record({})], + [IDL.Record({ 'batch_id' : BatchId })], + [], + ), + 'create_chunk' : IDL.Func( + [IDL.Record({ 'content' : IDL.Vec(IDL.Nat8), 'batch_id' : BatchId })], + [IDL.Record({ 'chunk_id' : ChunkId })], + [], + ), + 'deauthorize' : IDL.Func([IDL.Principal], [], []), + 'delete_asset' : IDL.Func([DeleteAssetArguments], [], []), + 'delete_batch' : IDL.Func([DeleteBatchArguments], [], []), + 'get' : IDL.Func( + [IDL.Record({ 'key' : Key, 'accept_encodings' : IDL.Vec(IDL.Text) })], + [ + IDL.Record({ + 'content' : IDL.Vec(IDL.Nat8), + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'content_type' : IDL.Text, + 'content_encoding' : IDL.Text, + 'total_length' : IDL.Nat, + }), + ], + ['query'], + ), + 'get_asset_properties' : IDL.Func( + [Key], + [ + IDL.Record({ + 'headers' : IDL.Opt(IDL.Vec(HeaderField)), + 'is_aliased' : IDL.Opt(IDL.Bool), + 'allow_raw_access' : IDL.Opt(IDL.Bool), + 'max_age' : IDL.Opt(IDL.Nat64), + }), + ], + ['query'], + ), + 'get_chunk' : IDL.Func( + [ + IDL.Record({ + 'key' : Key, + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'index' : IDL.Nat, + 'content_encoding' : IDL.Text, + }), + ], + [IDL.Record({ 'content' : IDL.Vec(IDL.Nat8) })], + ['query'], + ), + 'get_configuration' : IDL.Func([], [ConfigurationResponse], []), + 'grant_permission' : IDL.Func([GrantPermission], [], []), + 'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']), + 'http_request_streaming_callback' : IDL.Func( + [StreamingCallbackToken], + [IDL.Opt(StreamingCallbackHttpResponse)], + ['query'], + ), + 'list' : IDL.Func( + [IDL.Record({})], + [ + IDL.Vec( + IDL.Record({ + 'key' : Key, + 'encodings' : IDL.Vec( + IDL.Record({ + 'modified' : Time, + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'length' : IDL.Nat, + 'content_encoding' : IDL.Text, + }) + ), + 'content_type' : IDL.Text, + }) + ), + ], + ['query'], + ), + 'list_authorized' : IDL.Func([], [IDL.Vec(IDL.Principal)], []), + 'list_permitted' : IDL.Func([ListPermitted], [IDL.Vec(IDL.Principal)], []), + 'propose_commit_batch' : IDL.Func([CommitBatchArguments], [], []), + 'revoke_permission' : IDL.Func([RevokePermission], [], []), + 'set_asset_content' : IDL.Func([SetAssetContentArguments], [], []), + 'set_asset_properties' : IDL.Func([SetAssetPropertiesArguments], [], []), + 'store' : IDL.Func( + [ + IDL.Record({ + 'key' : Key, + 'content' : IDL.Vec(IDL.Nat8), + 'sha256' : IDL.Opt(IDL.Vec(IDL.Nat8)), + 'content_type' : IDL.Text, + 'content_encoding' : IDL.Text, + }), + ], + [], + [], + ), + 'take_ownership' : IDL.Func([], [], []), + 'unset_asset_content' : IDL.Func([UnsetAssetContentArguments], [], []), + 'validate_commit_proposed_batch' : IDL.Func( + [CommitProposedBatchArguments], + [ValidationResult], + [], + ), + 'validate_configure' : IDL.Func( + [ConfigureArguments], + [ValidationResult], + [], + ), + 'validate_grant_permission' : IDL.Func( + [GrantPermission], + [ValidationResult], + [], + ), + 'validate_revoke_permission' : IDL.Func( + [RevokePermission], + [ValidationResult], + [], + ), + 'validate_take_ownership' : IDL.Func([], [ValidationResult], []), + }); +}; +export const init = ({ IDL }) => { + const SetPermissions = IDL.Record({ + 'prepare' : IDL.Vec(IDL.Principal), + 'commit' : IDL.Vec(IDL.Principal), + 'manage_permissions' : IDL.Vec(IDL.Principal), + }); + const UpgradeArgs = IDL.Record({ + 'set_permissions' : IDL.Opt(SetPermissions), + }); + const InitArgs = IDL.Record({}); + const AssetCanisterArgs = IDL.Variant({ + 'Upgrade' : UpgradeArgs, + 'Init' : InitArgs, + }); + return [IDL.Opt(AssetCanisterArgs)]; +}; diff --git a/src/declarations/ekoke-erc20-swap-frontend/index.d.ts b/src/declarations/ekoke-erc20-swap-frontend/index.d.ts new file mode 100644 index 0000000..26053b0 --- /dev/null +++ b/src/declarations/ekoke-erc20-swap-frontend/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-erc20-swap-frontend.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-erc20-swap-frontend: ActorSubclass<_SERVICE>; diff --git a/src/declarations/ekoke-erc20-swap-frontend/index.js b/src/declarations/ekoke-erc20-swap-frontend/index.js new file mode 100644 index 0000000..4888bbe --- /dev/null +++ b/src/declarations/ekoke-erc20-swap-frontend/index.js @@ -0,0 +1,43 @@ +import { Actor, HttpAgent } from "@dfinity/agent"; + +// Imports and re-exports candid interface +import { idlFactory } from "./ekoke-erc20-swap-frontend.did.js"; +export { idlFactory } from "./ekoke-erc20-swap-frontend.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-ERC20-SWAP-FRONTEND || + process.env.EKOKE-ERC20-SWAP-FRONTEND_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-erc20-swap-frontend = canisterId ? createActor(canisterId) : undefined; diff --git a/src/ekoke_erc20_swap_frontend/package.json b/src/ekoke_erc20_swap_frontend/package.json index fe0ec78..562e9c5 100644 --- a/src/ekoke_erc20_swap_frontend/package.json +++ b/src/ekoke_erc20_swap_frontend/package.json @@ -28,6 +28,8 @@ "prettier:check": "prettier --config .prettierrc --check src/" }, "dependencies": { + "@dfinity/candid": "^1.0.1", + "@dfinity/principal": "^1.0.1", "metamask-react": "^2.7.0", "react": "^18.2", "react-dom": "^18.2", diff --git a/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/Summary.tsx b/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/Summary.tsx index b4ac6de..a0e53c0 100644 --- a/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/Summary.tsx +++ b/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/Summary.tsx @@ -12,6 +12,7 @@ import { CONTRACT_ADDRESS } from '../../../web3/contracts/Ekoke'; import Button from '../../reusable/Button'; import { Page, PageProps } from '../ConnectedPage'; import EkokeLogo from '../../../../assets/images/ekoke-logo.webp'; +import Paragraph from '../../reusable/Paragraph'; const Summary = ({ onSwitchPage }: PageProps) => { const { account, ethereum, chainId } = useConnectedMetaMask(); @@ -28,7 +29,7 @@ const Summary = ({ onSwitchPage }: PageProps) => { return ( - + EKOKE Token {
- + ERC20 Token Address:{' '} {CONTRACT_ADDRESS[chainId as ChainId]} - + - + ERC20 Swapped Supply: {swappedSupply} - + onSwitchPage(Page.IcrcToErc20)}> diff --git a/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/SwapErc20ToIcrc.tsx b/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/SwapErc20ToIcrc.tsx new file mode 100644 index 0000000..70b698a --- /dev/null +++ b/src/ekoke_erc20_swap_frontend/src/js/components/App/pages/SwapErc20ToIcrc.tsx @@ -0,0 +1,192 @@ +import * as React from 'react'; +import * as Icon from 'react-feather'; +import { useConnectedMetaMask } from 'metamask-react'; +import { useConnectedIcWallet } from 'react-ic-wallet'; + +import Web3Client from '../../../web3/Web3Client'; +import { ChainId } from '../../MetamaskConnect'; +import Container from '../../reusable/Container'; +import Heading from '../../reusable/Heading'; +import Link from '../../reusable/Link'; +import Button from '../../reusable/Button'; +import { Page, PageProps } from '../ConnectedPage'; +import Ethereum from '../../svg/Ethereum'; +import InternetComputer from '../../svg/InternetComputer'; +import EthereumWhite from '../../svg/EthereumWhite'; +import Input from '../../reusable/Input'; +import { e8sToEkoke, validatePrincipal } from '../../../utils'; +import Alerts from '../../reusable/Alerts'; +import { Principal } from '@dfinity/principal'; +import Paragraph from '../../reusable/Paragraph'; + +const SwapErc20ToIcrc = ({ onSwitchPage }: PageProps) => { + const { account, ethereum, chainId } = useConnectedMetaMask(); + const { principal } = useConnectedIcWallet(); + + const [recipientPrincipal, setRecipientPrincipal] = + React.useState(principal); + const [amount, setAmount] = React.useState(''); + const [userBalance, setUserBalance] = React.useState(); + const [processing, setProcessing] = React.useState(false); + const [error, setError] = React.useState(null); + const [success, setSuccess] = React.useState(false); + + const onRecipientPrincipalChange = ( + e: React.ChangeEvent, + ) => { + setRecipientPrincipal(e.target.value); + }; + const onAmountChange = (e: React.ChangeEvent) => { + setAmount(e.target.value); + }; + + const validateUserAmount = ( + amount: string | number | readonly string[] | undefined, + ): boolean => { + if (typeof amount !== 'string') return false; + + if (isNaN(parseInt(amount))) { + return false; + } + + if (userBalance === undefined) return true; + + const userAmount = BigInt(amount); + return userAmount <= userBalance.valueOf(); + }; + + const onSwap = () => { + setProcessing(true); + + // check if the user has enough balance + if (!validateUserAmount(amount)) { + setProcessing(false); + setError('Insufficient balance.'); + return; + } + if (!validatePrincipal(recipientPrincipal)) { + setProcessing(false); + setError('Invalid principal.'); + return; + } + + const principal = Principal.fromText(recipientPrincipal); + const numAmount = BigInt(amount); + + const web3 = new Web3Client(account, ethereum, chainId as ChainId); + web3 + .swap(principal, numAmount.valueOf()) + .then(() => { + setProcessing(false); + setAmount(''); + setError(null); + setSuccess(true); + setRecipientPrincipal(''); + }) + .catch((e) => { + setProcessing(false); + setError(`Swap failed: ${e.message}`); + setSuccess(false); + }); + }; + + React.useEffect(() => { + if (!ethereum || !account || !chainId) return; + + const web3 = new Web3Client(account, ethereum, chainId as ChainId); + + web3.balanceOf(account).then((balance) => { + setUserBalance(balance); + }); + }, [ethereum, account, chainId]); + + const disabled = !recipientPrincipal || !amount || processing; + + return ( + + + + + {!processing && ( + onSwitchPage(Page.Summary)} + > + + Back + + )} + + + + Swap ERC20 to ICRC + + + + {userBalance && ( + + Your EKOKE ERC20 balance: {e8sToEkoke(userBalance)} + + )} + + + + } + label="Recipient Principal" + id="recipient-principal" + placeholder={principal} + value={recipientPrincipal} + validate={validatePrincipal} + validationMessage="Please enter a valid principal." + onChange={onRecipientPrincipalChange} + /> + + + } + label="Amount" + id="amount" + value={amount} + placeholder="10000" + type="number" + validationMessage="Please enter a valid principal." + validate={validateUserAmount} + onChange={onAmountChange} + /> + + + {error && ( + + + {error} + + + )} + {success && ( + + + Swap successful! Wait up to 3 hours to see the swapped amount + on EKOKE ICRC . + + + )} + + {processing ? ( + + ) : ( + + )} + Swap + + + + + + ); +}; + +export default SwapErc20ToIcrc; diff --git a/src/ekoke_erc20_swap_frontend/src/js/components/AppLayout.tsx b/src/ekoke_erc20_swap_frontend/src/js/components/AppLayout.tsx index cb5f5e1..8569078 100644 --- a/src/ekoke_erc20_swap_frontend/src/js/components/AppLayout.tsx +++ b/src/ekoke_erc20_swap_frontend/src/js/components/AppLayout.tsx @@ -9,7 +9,7 @@ import Footer from './Footer'; const AppLayout = () => (
- +