Skip to content

Commit ae4d006

Browse files
committed
feat: import tokens
1 parent cffba43 commit ae4d006

25 files changed

+564
-144
lines changed

src/ic_message_frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,5 @@
8282
"test": "vitest run"
8383
},
8484
"type": "module",
85-
"version": "2.6.11"
85+
"version": "2.7.0"
8686
}

src/ic_message_frontend/src/declarations/ic_message_profile/ic_message_profile.did

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ProfileInfo = record {
3636
channels : opt vec record { record { principal; nat64 }; ChannelSetting };
3737
image_file : opt record { principal; nat32 };
3838
links : vec Link;
39+
tokens : vec principal;
3940
canister : principal;
4041
ecdh_pub : opt blob;
4142
following : opt vec principal;
@@ -87,6 +88,7 @@ service : (opt ChainArgs) -> {
8788
update_links : (vec Link) -> (Result);
8889
update_profile : (UpdateProfileInput) -> (Result_2);
8990
update_profile_ecdh_pub : (blob) -> (Result);
91+
update_tokens : (vec principal) -> (Result);
9092
upload_image_token : (UploadImageInput) -> (Result_4);
9193
validate2_admin_add_managers : (vec principal) -> (Result_5);
9294
validate2_admin_remove_managers : (vec principal) -> (Result_5);

src/ic_message_frontend/src/declarations/ic_message_profile/ic_message_profile.did.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface ProfileInfo {
5050
'channels' : [] | [Array<[[Principal, bigint], ChannelSetting]>],
5151
'image_file' : [] | [[Principal, number]],
5252
'links' : Array<Link>,
53+
'tokens' : Array<Principal>,
5354
'canister' : Principal,
5455
'ecdh_pub' : [] | [Uint8Array | number[]],
5556
'following' : [] | [Array<Principal>],
@@ -114,6 +115,7 @@ export interface _SERVICE {
114115
'update_links' : ActorMethod<[Array<Link>], Result>,
115116
'update_profile' : ActorMethod<[UpdateProfileInput], Result_2>,
116117
'update_profile_ecdh_pub' : ActorMethod<[Uint8Array | number[]], Result>,
118+
'update_tokens' : ActorMethod<[Array<Principal>], Result>,
117119
'upload_image_token' : ActorMethod<[UploadImageInput], Result_4>,
118120
'validate2_admin_add_managers' : ActorMethod<[Array<Principal>], Result_5>,
119121
'validate2_admin_remove_managers' : ActorMethod<[Array<Principal>], Result_5>,

src/ic_message_frontend/src/declarations/ic_message_profile/ic_message_profile.did.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export const idlFactory = ({ IDL }) => {
7272
),
7373
'image_file' : IDL.Opt(IDL.Tuple(IDL.Principal, IDL.Nat32)),
7474
'links' : IDL.Vec(Link),
75+
'tokens' : IDL.Vec(IDL.Principal),
7576
'canister' : IDL.Principal,
7677
'ecdh_pub' : IDL.Opt(IDL.Vec(IDL.Nat8)),
7778
'following' : IDL.Opt(IDL.Vec(IDL.Principal)),
@@ -129,6 +130,7 @@ export const idlFactory = ({ IDL }) => {
129130
'update_links' : IDL.Func([IDL.Vec(Link)], [Result], []),
130131
'update_profile' : IDL.Func([UpdateProfileInput], [Result_2], []),
131132
'update_profile_ecdh_pub' : IDL.Func([IDL.Vec(IDL.Nat8)], [Result], []),
133+
'update_tokens' : IDL.Func([IDL.Vec(IDL.Principal)], [Result], []),
132134
'upload_image_token' : IDL.Func([UploadImageInput], [Result_4], []),
133135
'validate2_admin_add_managers' : IDL.Func(
134136
[IDL.Vec(IDL.Principal)],

src/ic_message_frontend/src/lib/canisters/messageprofile.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,9 @@ export class ProfileAPI {
7979
const res = await this.actor.update_links(input)
8080
return unwrapResult(res, 'call update_links failed')
8181
}
82+
83+
async update_tokens(input: Principal[]): Promise<null> {
84+
const res = await this.actor.update_tokens(input)
85+
return unwrapResult(res, 'call update_tokens failed')
86+
}
8287
}

src/ic_message_frontend/src/lib/canisters/tokenledger.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,47 @@ export class TokenLedgerAPI {
2323
this.token = token
2424
}
2525

26+
static async fromID(canisterId: Principal): Promise<TokenLedgerAPI> {
27+
const actor = createActor<_SERVICE>({
28+
canisterId,
29+
idlFactory: idlFactory
30+
})
31+
const metadata = await actor.icrc1_metadata()
32+
const token: TokenInfo = {
33+
name: 'Internet Computer',
34+
symbol: 'ICP',
35+
decimals: 8,
36+
fee: 10000n,
37+
one: 100000000n,
38+
logo: '',
39+
canisterId: canisterId.toText()
40+
}
41+
42+
for (const [key, value] of metadata) {
43+
switch (key) {
44+
case 'icrc1:name':
45+
token.name = (value as { 'Text': string }).Text
46+
continue
47+
case 'icrc1:symbol':
48+
token.symbol = (value as { 'Text': string }).Text
49+
continue
50+
case 'icrc1:decimals':
51+
const decimals = (value as { 'Nat': bigint }).Nat
52+
token.decimals = Number(decimals)
53+
token.one = 10n ** decimals
54+
continue
55+
case 'icrc1:fee':
56+
token.fee = (value as { 'Nat': bigint }).Nat
57+
continue
58+
case 'icrc1:logo':
59+
token.logo = (value as { 'Text': string }).Text
60+
continue
61+
}
62+
}
63+
64+
return new TokenLedgerAPI(token)
65+
}
66+
2667
async balance(): Promise<bigint> {
2768
return this.getBalanceOf(agent.id.getPrincipal())
2869
}
@@ -80,4 +121,4 @@ export class TokenLedgerAPI {
80121
}
81122
}
82123

83-
export const tokenLedgerAPI = new TokenLedgerAPI(PANDAToken)
124+
export const pandaLedgerAPI = new TokenLedgerAPI(PANDAToken)
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<script lang="ts">
2+
import { TokenLedgerAPI } from '$lib/canisters/tokenledger'
3+
import IconCircleSpin from '$lib/components/icons/IconCircleSpin.svelte'
4+
import IconExternalLinkLine from '$lib/components/icons/IconExternalLinkLine.svelte'
5+
import ModalCard from '$lib/components/ui/ModalCard.svelte'
6+
import type { LedgerAPI } from '$lib/types/token'
7+
import { TokenDisplay, type TokenInfo } from '$lib/utils/token'
8+
import { Principal } from '@dfinity/principal'
9+
import { tick, type SvelteComponent } from 'svelte'
10+
11+
// Props
12+
13+
type TokenInfoEx = TokenInfo & { api: LedgerAPI; balance: bigint }
14+
15+
interface Props {
16+
/** Exposes parent props to this component. */
17+
parent: SvelteComponent
18+
onsave: (token: TokenInfoEx) => Promise<void>
19+
}
20+
21+
let { parent, onsave }: Props = $props()
22+
23+
let ledgerId = $state('')
24+
let ledgerErr = $state('')
25+
let token: TokenInfoEx | null = $state(null)
26+
let submitting = $state(false)
27+
28+
async function onFormChange(e: Event) {
29+
e.stopPropagation()
30+
e.preventDefault()
31+
32+
ledgerErr = ''
33+
let id: Principal | null = null
34+
token = null
35+
try {
36+
id = Principal.fromText(ledgerId)
37+
} catch (e) {}
38+
39+
if (!id) {
40+
return
41+
}
42+
43+
try {
44+
submitting = true
45+
await tick()
46+
const api = await TokenLedgerAPI.fromID(id)
47+
const balance = await api.balance()
48+
token = { ...api.token, api, balance }
49+
} catch (e) {
50+
token = null
51+
ledgerErr = 'Token not found'
52+
}
53+
54+
submitting = false
55+
}
56+
57+
async function onclick(e: Event) {
58+
if (token) {
59+
submitting = true
60+
await onsave(token)
61+
parent['onClose'] && parent['onClose']()
62+
}
63+
}
64+
</script>
65+
66+
<ModalCard {parent}>
67+
<div class="!mt-0 text-center text-xl font-bold">Import Token</div>
68+
69+
<form
70+
class="m-auto !mt-4 flex flex-col content-center"
71+
oninput={onFormChange}
72+
>
73+
<div class="">
74+
<p class="">
75+
You can import a new token to wallet by providing its ledger canister
76+
ID.
77+
</p>
78+
<a
79+
type="button"
80+
class="mt-2 flex w-fit flex-row items-center gap-2 text-primary-500"
81+
target="_blank"
82+
href="https://internetcomputer.org/docs/current/developer-docs/daos/nns/using-the-nns-dapp/nns-dapp-importing-tokens"
83+
>
84+
<span class="*:size-5"><IconExternalLinkLine /></span>
85+
<span class="hover:underline">How to find ICRC token ledger</span>
86+
</a>
87+
</div>
88+
<div class="relative mt-4">
89+
<input
90+
class="border-gray/10 input truncate rounded-xl bg-white/20 invalid:input-warning"
91+
type="text"
92+
name="ledgerId"
93+
maxlength="27"
94+
data-1p-ignore
95+
bind:value={ledgerId}
96+
disabled={submitting}
97+
placeholder="_____-_____-_____-_____-cai"
98+
required
99+
/>
100+
</div>
101+
{#if token}
102+
<div class="mt-4 grid grid-cols-[48px_1fr]">
103+
<img class="size-12" alt={token.name} src={token.logo} />
104+
<div class="pl-2">
105+
<div class="flex flex-row justify-between">
106+
<span class="">{token.symbol}</span>
107+
<span class=""
108+
>{new TokenDisplay(token, token.balance).display()}</span
109+
>
110+
</div>
111+
<div class="flex flex-row justify-between text-sm text-surface-500">
112+
<span class="">{token.name}</span>
113+
</div>
114+
</div>
115+
</div>
116+
{:else if ledgerErr}
117+
<div class="mt-4">
118+
<p class="text-error-500">{ledgerErr}</p>
119+
</div>
120+
{/if}
121+
</form>
122+
<footer class="m-auto !mt-6">
123+
<button
124+
class="variant-filled-primary btn w-full"
125+
disabled={submitting || !token}
126+
{onclick}
127+
>
128+
{#if submitting}
129+
<span class=""><IconCircleSpin /></span>
130+
<span>Processing...</span>
131+
{:else}
132+
<span>Import</span>
133+
{/if}
134+
</button>
135+
</footer>
136+
</ModalCard>
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script lang="ts">
2+
import ModalCard from '$lib/components/ui/ModalCard.svelte'
3+
import SendTokenForm from '$lib/components/ui/SendTokenForm.svelte'
4+
import type { LedgerAPI, SendTokenArgs } from '$lib/types/token'
5+
import { type TokenInfo } from '$lib/utils/token'
6+
import type { Principal } from '@dfinity/principal'
7+
import { type SvelteComponent } from 'svelte'
8+
9+
// Props
10+
11+
type TokenInfoEx = TokenInfo & { api: LedgerAPI; balance: bigint }
12+
13+
interface Props {
14+
/** Exposes parent props to this component. */
15+
parent: SvelteComponent
16+
token: TokenInfoEx
17+
principal: Principal
18+
}
19+
20+
let { parent, principal, token }: Props = $props()
21+
22+
async function handleTokenTransfer(args: SendTokenArgs) {
23+
const idx = await token.api.transfer(args.to, args.amount)
24+
token.balance = await token.api.balance()
25+
return idx
26+
}
27+
</script>
28+
29+
<ModalCard {parent}>
30+
<div class="!mt-0 text-center text-xl font-bold">Transfer {token.symbol}</div>
31+
<div class="mx-auto !mt-6 space-y-4">
32+
<SendTokenForm
33+
sendFrom={principal}
34+
availableBalance={token.balance}
35+
{token}
36+
onSubmit={handleTokenTransfer}
37+
/>
38+
</div>
39+
</ModalCard>

0 commit comments

Comments
 (0)