Skip to content

Commit ca3f1d5

Browse files
authored
FT (#331)
FT
1 parent cccfe71 commit ca3f1d5

File tree

38 files changed

+2055
-21
lines changed

38 files changed

+2055
-21
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
import { NEAR, Worker } from "near-workspaces";
2+
import test from "ava";
3+
4+
const INITIAL_BALANCE = NEAR.parse("10000 N").toJSON();
5+
const ONE_YOCTO = "1";
6+
const STOARAGE_BYTE_COST = 10_000_000_000_000_000_000n;
7+
const ACCOUNT_STORAGE_BALANCE = String(STOARAGE_BYTE_COST * 138n);
8+
9+
test.beforeEach(async (t) => {
10+
const worker = await Worker.init();
11+
const root = worker.rootAccount;
12+
13+
const ftContract = await root.devDeploy("./build/my-ft.wasm");
14+
await ftContract.call(
15+
ftContract,
16+
"init_with_default_meta",
17+
{
18+
owner_id: ftContract.accountId,
19+
total_supply: INITIAL_BALANCE
20+
}
21+
);
22+
23+
/**
24+
* DEFI contract implemented in https://github.com/near/near-sdk-rs/tree/master/examples/fungible-token/test-contract-defi
25+
* Iterface:
26+
* pub fn new(fungible_token_account_id: AccountId) -> Self;
27+
* fn ft_on_transfer(
28+
&mut self,
29+
sender_id: AccountId,
30+
amount: U128,
31+
msg: String,
32+
) -> PromiseOrValue<U128>
33+
* If given `msg: "take-my-money", immediately returns U128::From(0). Otherwise, makes a cross-contract call to own `value_please` function, passing `msg` value_please will attempt to parse `msg` as an integer and return a U128 version of it
34+
*/
35+
const defiContract = await root.devDeploy("./res/defi.wasm");
36+
37+
await defiContract.call(
38+
defiContract,
39+
"new",
40+
{
41+
fungible_token_account_id: ftContract.accountId
42+
}
43+
);
44+
45+
const alice = await root.createSubAccount("alice", { initialBalance: NEAR.parse("10 N").toJSON() });
46+
47+
await registerUser(ftContract, alice.accountId);
48+
49+
t.context.worker = worker;
50+
t.context.accounts = {
51+
root,
52+
ftContract,
53+
alice,
54+
defiContract,
55+
};
56+
});
57+
58+
test.afterEach.always(async (t) => {
59+
await t.context.worker.tearDown().catch((error) => {
60+
console.log("Failed tear down the worker:", error);
61+
});
62+
});
63+
64+
65+
async function registerUser(contract, account_id) {
66+
const deposit = String(ACCOUNT_STORAGE_BALANCE);
67+
await contract.call(contract, "storage_deposit", { account_id: account_id }, { attachedDeposit: deposit });
68+
}
69+
70+
test("test_total_supply", async (t) => {
71+
const { ftContract } = t.context.accounts;
72+
const res = await ftContract.view("ft_total_supply", {});
73+
t.is(BigInt(res), BigInt(INITIAL_BALANCE));
74+
});
75+
76+
test("test_storage_deposit", async (t) => {
77+
const { ftContract, root } = t.context.accounts;
78+
const bob = await root.createSubAccount("bob", { initialBalance: NEAR.parse("10 N").toJSON() });
79+
await registerUser(ftContract, bob.accountId);
80+
const bobStorageBalance = await ftContract.view("storage_balance_of", { account_id: bob.accountId });
81+
t.is(bobStorageBalance.total, String(ACCOUNT_STORAGE_BALANCE));
82+
});
83+
84+
test("test_simple_transfer", async (t) => {
85+
const TRANSFER_AMOUNT = NEAR.parse("1000 N").toJSON();
86+
const EXPECTED_ROOT_BALANCE = NEAR.parse("9000 N").toJSON();
87+
88+
const { ftContract, alice } = t.context.accounts;
89+
90+
await ftContract.call(
91+
ftContract,
92+
"ft_transfer",
93+
{
94+
receiver_id: alice.accountId,
95+
amount: TRANSFER_AMOUNT,
96+
memo: null
97+
},
98+
{
99+
attachedDeposit: ONE_YOCTO
100+
}
101+
);
102+
103+
let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId });
104+
105+
let alice_balance = await ftContract.view("ft_balance_of", { account_id: alice.accountId });
106+
107+
t.is(EXPECTED_ROOT_BALANCE, root_balance);
108+
t.is(TRANSFER_AMOUNT, alice_balance);
109+
});
110+
111+
test("test_close_account_empty_balance", async (t) => {
112+
const { ftContract, alice } = t.context.accounts;
113+
114+
let res = await alice.call(ftContract, "storage_unregister", {}, { attachedDeposit: ONE_YOCTO });
115+
t.is(res, true); // TODO: doublecheck
116+
});
117+
118+
test("test_close_account_non_empty_balance", async (t) => {
119+
const { ftContract } = t.context.accounts;
120+
121+
try {
122+
await ftContract.call(ftContract, "storage_unregister", {}, { attachedDeposit: ONE_YOCTO });
123+
throw Error("Unreachable string");
124+
} catch (e) {
125+
t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("Can't unregister the account with the positive balance without force"), true);
126+
}
127+
128+
try {
129+
await ftContract.call(ftContract, "storage_unregister", { force: false }, { attachedDeposit: ONE_YOCTO });
130+
throw Error("Unreachable string");
131+
} catch (e) {
132+
t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("Can't unregister the account with the positive balance without force"), true);
133+
}
134+
});
135+
136+
test("simulate_close_account_force_non_empty_balance", async (t) => {
137+
const { ftContract } = t.context.accounts;
138+
139+
await ftContract.call(
140+
ftContract,
141+
"storage_unregister",
142+
{ force: true },
143+
{ attachedDeposit: ONE_YOCTO }
144+
);
145+
146+
const res = await ftContract.view("ft_total_supply", {});
147+
t.is(res, "0");
148+
});
149+
150+
test("simulate_transfer_call_with_burned_amount", async (t) => {
151+
const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON();
152+
153+
const { ftContract, defiContract } = t.context.accounts;
154+
155+
// defi contract must be registered as a FT account
156+
await registerUser(ftContract, defiContract.accountId);
157+
158+
const result = await ftContract
159+
.batch(ftContract)
160+
.functionCall(
161+
'ft_transfer_call',
162+
{
163+
receiver_id: defiContract.accountId,
164+
amount: TRANSFER_AMOUNT,
165+
memo: null,
166+
msg: "10",
167+
},
168+
{
169+
attachedDeposit: '1',
170+
gas: '150 Tgas'
171+
},
172+
)
173+
.functionCall(
174+
'storage_unregister',
175+
{
176+
force: true
177+
},
178+
{
179+
attachedDeposit: '1',
180+
gas: '150 Tgas',
181+
},
182+
)
183+
.transact();
184+
185+
const logs = JSON.stringify(result);
186+
let expected = `Account @${ftContract.accountId} burned ${10}`;
187+
t.is(logs.includes("The account of the sender was deleted"), true);
188+
t.is(logs.includes(expected), true);
189+
190+
const new_total_supply = await ftContract.view("ft_total_supply", {});
191+
192+
t.is(BigInt(new_total_supply), BigInt(TRANSFER_AMOUNT) - 10n);
193+
194+
const defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId });
195+
196+
t.is(BigInt(defi_balance), BigInt(TRANSFER_AMOUNT) - 10n);
197+
});
198+
199+
test("simulate_transfer_call_with_immediate_return_and_no_refund", async (t) => {
200+
const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON();
201+
202+
const { ftContract, defiContract } = t.context.accounts;
203+
204+
// defi ftContract must be registered as a FT account
205+
await registerUser(ftContract, defiContract.accountId);
206+
207+
// root invests in defi by calling `ft_transfer_call`
208+
await ftContract.call(
209+
ftContract,
210+
"ft_transfer_call",
211+
{
212+
receiver_id: defiContract.accountId,
213+
amount: TRANSFER_AMOUNT,
214+
memo: null,
215+
msg: "take-my-money"
216+
},
217+
{
218+
attachedDeposit: ONE_YOCTO,
219+
gas: 300000000000000,
220+
}
221+
);
222+
223+
let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId });
224+
let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId });
225+
226+
t.is(BigInt(INITIAL_BALANCE) - BigInt(TRANSFER_AMOUNT), BigInt(root_balance));
227+
t.is(TRANSFER_AMOUNT, defi_balance);
228+
});
229+
230+
test("simulate_transfer_call_when_called_contract_not_registered_with_ft", async (t) => {
231+
const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON();
232+
233+
const { ftContract, defiContract } = t.context.accounts;
234+
235+
// call fails because DEFI contract is not registered as FT user
236+
try {
237+
await ftContract.call(
238+
ftContract,
239+
"ft_transfer_call",
240+
{
241+
receiver_id: defiContract.accountId,
242+
amount: TRANSFER_AMOUNT,
243+
memo: null,
244+
msg: "take-my-money"
245+
},
246+
{
247+
attachedDeposit: ONE_YOCTO,
248+
gas: 50000000000000n,
249+
}
250+
);
251+
t.is(true, false); // Unreachable
252+
} catch (e) {
253+
t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("is not registered"), true);
254+
}
255+
256+
// balances remain unchanged
257+
let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId });
258+
let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId });
259+
260+
t.is(BigInt(INITIAL_BALANCE), BigInt(root_balance));
261+
t.is("0", defi_balance);
262+
});
263+
264+
test("simulate_transfer_call_with_promise_and_refund", async (t) => {
265+
const REFUND_AMOUNT = NEAR.parse("50 N").toJSON();
266+
const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON();
267+
const TRANSFER_CALL_GAS = String(300_000_000_000_000n);
268+
269+
const { ftContract, defiContract } = t.context.accounts;
270+
271+
// defi contract must be registered as a FT account
272+
await registerUser(ftContract, defiContract.accountId);
273+
274+
await ftContract.call(ftContract, "ft_transfer_call", {
275+
receiver_id: defiContract.accountId,
276+
amount: TRANSFER_AMOUNT,
277+
memo: null,
278+
msg: REFUND_AMOUNT,
279+
}, {
280+
attachedDeposit: ONE_YOCTO,
281+
gas: TRANSFER_CALL_GAS,
282+
});
283+
284+
let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId });
285+
let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId });
286+
287+
t.is(BigInt(INITIAL_BALANCE) - BigInt(TRANSFER_AMOUNT) + BigInt(REFUND_AMOUNT), BigInt(root_balance));
288+
t.is(BigInt(TRANSFER_AMOUNT) - BigInt(REFUND_AMOUNT), BigInt(defi_balance));
289+
});
290+
291+
test("simulate_transfer_call_promise_panics_for_a_full_refund", async (t) => {
292+
const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON();
293+
294+
const { ftContract, defiContract } = t.context.accounts;
295+
296+
// defi contract must be registered as a FT account
297+
await registerUser(ftContract, defiContract.accountId);
298+
299+
// root invests in defi by calling `ft_transfer_call`
300+
const res = await ftContract.callRaw(
301+
ftContract,
302+
"ft_transfer_call",
303+
{
304+
receiver_id: defiContract.accountId,
305+
amount: TRANSFER_AMOUNT,
306+
memo: null,
307+
msg: "no parsey as integer big panic oh no",
308+
},
309+
{
310+
attachedDeposit: ONE_YOCTO,
311+
gas: 50000000000000n,
312+
}
313+
);
314+
315+
t.is(JSON.stringify(res).includes("ParseIntError"), true);
316+
317+
// balances remain unchanged
318+
let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId });
319+
let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId });
320+
321+
t.is(INITIAL_BALANCE, root_balance);
322+
t.is("0", defi_balance);
323+
});

examples/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,10 @@
2424
"build:nft-contract": "near-sdk-js build src/standard-nft/my-nft.ts build/my-nft.wasm",
2525
"build:nft-receiver": "near-sdk-js build src/standard-nft/test-token-receiver.ts build/nft-receiver.wasm",
2626
"build:nft-approval-receiver": "near-sdk-js build src/standard-nft/test-approval-receiver.ts build/nft-approval-receiver.wasm",
27+
"build:ft": "near-sdk-js build src/standard-ft/my-ft.ts build/my-ft.wasm",
2728
"test": "ava && pnpm test:counter-lowlevel && pnpm test:counter-ts",
2829
"test:nft": "ava __tests__/standard-nft/*",
30+
"test:ft": "ava __tests__/standard-ft/*",
2931
"test:status-message": "ava __tests__/test-status-message.ava.js",
3032
"test:clean-state": "ava __tests__/test-clean-state.ava.js",
3133
"test:counter": "ava __tests__/test-counter.ava.js",

examples/res/defi.wasm

106 KB
Binary file not shown.

0 commit comments

Comments
 (0)