From ca3f1d5faa303ed3829c2cb59aedbdd15b6419e9 Mon Sep 17 00:00:00 2001 From: Serhii Volovyk Date: Tue, 21 Feb 2023 11:07:17 +0200 Subject: [PATCH] FT (#331) FT --- .../__tests__/standard-ft/ft-tests.ava.js | 323 +++++++++++++++ examples/package.json | 2 + examples/res/defi.wasm | Bin 0 -> 108959 bytes examples/src/standard-ft/my-ft.ts | 195 +++++++++ examples/src/standard-nft/my-nft.ts | 13 +- packages/near-contract-standards/README.md | 5 + .../lib/fungible_token/core.d.ts | 58 +++ .../lib/fungible_token/core.js | 1 + .../lib/fungible_token/core_impl.d.ts | 89 ++++ .../lib/fungible_token/core_impl.js | 276 +++++++++++++ .../lib/fungible_token/events.d.ts | 73 ++++ .../lib/fungible_token/events.js | 93 +++++ .../lib/fungible_token/index.d.ts | 6 + .../lib/fungible_token/index.js | 6 + .../lib/fungible_token/metadata.d.ts | 15 + .../lib/fungible_token/metadata.js | 22 + .../lib/fungible_token/receiver.d.ts | 28 ++ .../lib/fungible_token/receiver.js | 1 + .../lib/fungible_token/resolver.d.ts | 8 + .../lib/fungible_token/resolver.js | 1 + .../near-contract-standards/lib/index.d.ts | 5 +- packages/near-contract-standards/lib/index.js | 5 +- .../lib/storage_management/index.d.ts | 58 +++ .../lib/storage_management/index.js | 12 + packages/near-contract-standards/src/event.ts | 1 + .../src/fungible_token/core.ts | 72 ++++ .../src/fungible_token/core_impl.ts | 381 ++++++++++++++++++ .../src/fungible_token/events.ts | 127 ++++++ .../src/fungible_token/index.ts | 6 + .../src/fungible_token/metadata.ts | 49 +++ .../src/fungible_token/receiver.ts | 34 ++ .../src/fungible_token/resolver.ts | 13 + packages/near-contract-standards/src/index.ts | 6 +- .../src/storage_management/index.ts | 72 ++++ .../near-contract-standards/tsconfig.json | 2 - packages/near-sdk-js/lib/promise.d.ts | 6 +- packages/near-sdk-js/lib/promise.js | 6 +- packages/near-sdk-js/src/promise.ts | 6 +- 38 files changed, 2055 insertions(+), 21 deletions(-) create mode 100644 examples/__tests__/standard-ft/ft-tests.ava.js create mode 100755 examples/res/defi.wasm create mode 100644 examples/src/standard-ft/my-ft.ts create mode 100644 packages/near-contract-standards/README.md create mode 100644 packages/near-contract-standards/lib/fungible_token/core.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/core.js create mode 100644 packages/near-contract-standards/lib/fungible_token/core_impl.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/core_impl.js create mode 100644 packages/near-contract-standards/lib/fungible_token/events.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/events.js create mode 100644 packages/near-contract-standards/lib/fungible_token/index.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/index.js create mode 100644 packages/near-contract-standards/lib/fungible_token/metadata.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/metadata.js create mode 100644 packages/near-contract-standards/lib/fungible_token/receiver.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/receiver.js create mode 100644 packages/near-contract-standards/lib/fungible_token/resolver.d.ts create mode 100644 packages/near-contract-standards/lib/fungible_token/resolver.js create mode 100644 packages/near-contract-standards/lib/storage_management/index.d.ts create mode 100644 packages/near-contract-standards/lib/storage_management/index.js create mode 100644 packages/near-contract-standards/src/fungible_token/core.ts create mode 100644 packages/near-contract-standards/src/fungible_token/core_impl.ts create mode 100644 packages/near-contract-standards/src/fungible_token/events.ts create mode 100644 packages/near-contract-standards/src/fungible_token/index.ts create mode 100644 packages/near-contract-standards/src/fungible_token/metadata.ts create mode 100644 packages/near-contract-standards/src/fungible_token/receiver.ts create mode 100644 packages/near-contract-standards/src/fungible_token/resolver.ts create mode 100644 packages/near-contract-standards/src/storage_management/index.ts diff --git a/examples/__tests__/standard-ft/ft-tests.ava.js b/examples/__tests__/standard-ft/ft-tests.ava.js new file mode 100644 index 000000000..7520a2385 --- /dev/null +++ b/examples/__tests__/standard-ft/ft-tests.ava.js @@ -0,0 +1,323 @@ +import { NEAR, Worker } from "near-workspaces"; +import test from "ava"; + +const INITIAL_BALANCE = NEAR.parse("10000 N").toJSON(); +const ONE_YOCTO = "1"; +const STOARAGE_BYTE_COST = 10_000_000_000_000_000_000n; +const ACCOUNT_STORAGE_BALANCE = String(STOARAGE_BYTE_COST * 138n); + +test.beforeEach(async (t) => { + const worker = await Worker.init(); + const root = worker.rootAccount; + + const ftContract = await root.devDeploy("./build/my-ft.wasm"); + await ftContract.call( + ftContract, + "init_with_default_meta", + { + owner_id: ftContract.accountId, + total_supply: INITIAL_BALANCE + } + ); + + /** + * DEFI contract implemented in https://github.com/near/near-sdk-rs/tree/master/examples/fungible-token/test-contract-defi + * Iterface: + * pub fn new(fungible_token_account_id: AccountId) -> Self; + * fn ft_on_transfer( + &mut self, + sender_id: AccountId, + amount: U128, + msg: String, + ) -> PromiseOrValue + * 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 + */ + const defiContract = await root.devDeploy("./res/defi.wasm"); + + await defiContract.call( + defiContract, + "new", + { + fungible_token_account_id: ftContract.accountId + } + ); + + const alice = await root.createSubAccount("alice", { initialBalance: NEAR.parse("10 N").toJSON() }); + + await registerUser(ftContract, alice.accountId); + + t.context.worker = worker; + t.context.accounts = { + root, + ftContract, + alice, + defiContract, + }; +}); + +test.afterEach.always(async (t) => { + await t.context.worker.tearDown().catch((error) => { + console.log("Failed tear down the worker:", error); + }); +}); + + +async function registerUser(contract, account_id) { + const deposit = String(ACCOUNT_STORAGE_BALANCE); + await contract.call(contract, "storage_deposit", { account_id: account_id }, { attachedDeposit: deposit }); +} + +test("test_total_supply", async (t) => { + const { ftContract } = t.context.accounts; + const res = await ftContract.view("ft_total_supply", {}); + t.is(BigInt(res), BigInt(INITIAL_BALANCE)); +}); + +test("test_storage_deposit", async (t) => { + const { ftContract, root } = t.context.accounts; + const bob = await root.createSubAccount("bob", { initialBalance: NEAR.parse("10 N").toJSON() }); + await registerUser(ftContract, bob.accountId); + const bobStorageBalance = await ftContract.view("storage_balance_of", { account_id: bob.accountId }); + t.is(bobStorageBalance.total, String(ACCOUNT_STORAGE_BALANCE)); +}); + +test("test_simple_transfer", async (t) => { + const TRANSFER_AMOUNT = NEAR.parse("1000 N").toJSON(); + const EXPECTED_ROOT_BALANCE = NEAR.parse("9000 N").toJSON(); + + const { ftContract, alice } = t.context.accounts; + + await ftContract.call( + ftContract, + "ft_transfer", + { + receiver_id: alice.accountId, + amount: TRANSFER_AMOUNT, + memo: null + }, + { + attachedDeposit: ONE_YOCTO + } + ); + + let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId }); + + let alice_balance = await ftContract.view("ft_balance_of", { account_id: alice.accountId }); + + t.is(EXPECTED_ROOT_BALANCE, root_balance); + t.is(TRANSFER_AMOUNT, alice_balance); +}); + +test("test_close_account_empty_balance", async (t) => { + const { ftContract, alice } = t.context.accounts; + + let res = await alice.call(ftContract, "storage_unregister", {}, { attachedDeposit: ONE_YOCTO }); + t.is(res, true); // TODO: doublecheck +}); + +test("test_close_account_non_empty_balance", async (t) => { + const { ftContract } = t.context.accounts; + + try { + await ftContract.call(ftContract, "storage_unregister", {}, { attachedDeposit: ONE_YOCTO }); + throw Error("Unreachable string"); + } catch (e) { + t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("Can't unregister the account with the positive balance without force"), true); + } + + try { + await ftContract.call(ftContract, "storage_unregister", { force: false }, { attachedDeposit: ONE_YOCTO }); + throw Error("Unreachable string"); + } catch (e) { + t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("Can't unregister the account with the positive balance without force"), true); + } +}); + +test("simulate_close_account_force_non_empty_balance", async (t) => { + const { ftContract } = t.context.accounts; + + await ftContract.call( + ftContract, + "storage_unregister", + { force: true }, + { attachedDeposit: ONE_YOCTO } + ); + + const res = await ftContract.view("ft_total_supply", {}); + t.is(res, "0"); +}); + +test("simulate_transfer_call_with_burned_amount", async (t) => { + const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON(); + + const { ftContract, defiContract } = t.context.accounts; + + // defi contract must be registered as a FT account + await registerUser(ftContract, defiContract.accountId); + + const result = await ftContract + .batch(ftContract) + .functionCall( + 'ft_transfer_call', + { + receiver_id: defiContract.accountId, + amount: TRANSFER_AMOUNT, + memo: null, + msg: "10", + }, + { + attachedDeposit: '1', + gas: '150 Tgas' + }, + ) + .functionCall( + 'storage_unregister', + { + force: true + }, + { + attachedDeposit: '1', + gas: '150 Tgas', + }, + ) + .transact(); + + const logs = JSON.stringify(result); + let expected = `Account @${ftContract.accountId} burned ${10}`; + t.is(logs.includes("The account of the sender was deleted"), true); + t.is(logs.includes(expected), true); + + const new_total_supply = await ftContract.view("ft_total_supply", {}); + + t.is(BigInt(new_total_supply), BigInt(TRANSFER_AMOUNT) - 10n); + + const defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId }); + + t.is(BigInt(defi_balance), BigInt(TRANSFER_AMOUNT) - 10n); +}); + +test("simulate_transfer_call_with_immediate_return_and_no_refund", async (t) => { + const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON(); + + const { ftContract, defiContract } = t.context.accounts; + + // defi ftContract must be registered as a FT account + await registerUser(ftContract, defiContract.accountId); + + // root invests in defi by calling `ft_transfer_call` + await ftContract.call( + ftContract, + "ft_transfer_call", + { + receiver_id: defiContract.accountId, + amount: TRANSFER_AMOUNT, + memo: null, + msg: "take-my-money" + }, + { + attachedDeposit: ONE_YOCTO, + gas: 300000000000000, + } + ); + + let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId }); + let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId }); + + t.is(BigInt(INITIAL_BALANCE) - BigInt(TRANSFER_AMOUNT), BigInt(root_balance)); + t.is(TRANSFER_AMOUNT, defi_balance); +}); + +test("simulate_transfer_call_when_called_contract_not_registered_with_ft", async (t) => { + const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON(); + + const { ftContract, defiContract } = t.context.accounts; + + // call fails because DEFI contract is not registered as FT user + try { + await ftContract.call( + ftContract, + "ft_transfer_call", + { + receiver_id: defiContract.accountId, + amount: TRANSFER_AMOUNT, + memo: null, + msg: "take-my-money" + }, + { + attachedDeposit: ONE_YOCTO, + gas: 50000000000000n, + } + ); + t.is(true, false); // Unreachable + } catch (e) { + t.is(JSON.stringify(e, Object.getOwnPropertyNames(e)).includes("is not registered"), true); + } + + // balances remain unchanged + let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId }); + let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId }); + + t.is(BigInt(INITIAL_BALANCE), BigInt(root_balance)); + t.is("0", defi_balance); +}); + +test("simulate_transfer_call_with_promise_and_refund", async (t) => { + const REFUND_AMOUNT = NEAR.parse("50 N").toJSON(); + const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON(); + const TRANSFER_CALL_GAS = String(300_000_000_000_000n); + + const { ftContract, defiContract } = t.context.accounts; + + // defi contract must be registered as a FT account + await registerUser(ftContract, defiContract.accountId); + + await ftContract.call(ftContract, "ft_transfer_call", { + receiver_id: defiContract.accountId, + amount: TRANSFER_AMOUNT, + memo: null, + msg: REFUND_AMOUNT, + }, { + attachedDeposit: ONE_YOCTO, + gas: TRANSFER_CALL_GAS, + }); + + let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId }); + let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId }); + + t.is(BigInt(INITIAL_BALANCE) - BigInt(TRANSFER_AMOUNT) + BigInt(REFUND_AMOUNT), BigInt(root_balance)); + t.is(BigInt(TRANSFER_AMOUNT) - BigInt(REFUND_AMOUNT), BigInt(defi_balance)); +}); + +test("simulate_transfer_call_promise_panics_for_a_full_refund", async (t) => { + const TRANSFER_AMOUNT = NEAR.parse("100 N").toJSON(); + + const { ftContract, defiContract } = t.context.accounts; + + // defi contract must be registered as a FT account + await registerUser(ftContract, defiContract.accountId); + + // root invests in defi by calling `ft_transfer_call` + const res = await ftContract.callRaw( + ftContract, + "ft_transfer_call", + { + receiver_id: defiContract.accountId, + amount: TRANSFER_AMOUNT, + memo: null, + msg: "no parsey as integer big panic oh no", + }, + { + attachedDeposit: ONE_YOCTO, + gas: 50000000000000n, + } + ); + + t.is(JSON.stringify(res).includes("ParseIntError"), true); + + // balances remain unchanged + let root_balance = await ftContract.view("ft_balance_of", { account_id: ftContract.accountId }); + let defi_balance = await ftContract.view("ft_balance_of", { account_id: defiContract.accountId }); + + t.is(INITIAL_BALANCE, root_balance); + t.is("0", defi_balance); +}); diff --git a/examples/package.json b/examples/package.json index 128182fa9..fa83e7ded 100644 --- a/examples/package.json +++ b/examples/package.json @@ -24,8 +24,10 @@ "build:nft-contract": "near-sdk-js build src/standard-nft/my-nft.ts build/my-nft.wasm", "build:nft-receiver": "near-sdk-js build src/standard-nft/test-token-receiver.ts build/nft-receiver.wasm", "build:nft-approval-receiver": "near-sdk-js build src/standard-nft/test-approval-receiver.ts build/nft-approval-receiver.wasm", + "build:ft": "near-sdk-js build src/standard-ft/my-ft.ts build/my-ft.wasm", "test": "ava && pnpm test:counter-lowlevel && pnpm test:counter-ts", "test:nft": "ava __tests__/standard-nft/*", + "test:ft": "ava __tests__/standard-ft/*", "test:status-message": "ava __tests__/test-status-message.ava.js", "test:clean-state": "ava __tests__/test-clean-state.ava.js", "test:counter": "ava __tests__/test-counter.ava.js", diff --git a/examples/res/defi.wasm b/examples/res/defi.wasm new file mode 100755 index 0000000000000000000000000000000000000000..25d438c8b999ed230caabc9d594ad01eed99bf31 GIT binary patch literal 108959 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�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 WD@J*bbdK|iiyTrO>S0?rAO8nTknyqr literal 0 HcmV?d00001 diff --git a/examples/src/standard-ft/my-ft.ts b/examples/src/standard-ft/my-ft.ts new file mode 100644 index 000000000..fb3a9c67a --- /dev/null +++ b/examples/src/standard-ft/my-ft.ts @@ -0,0 +1,195 @@ +import { + StorageBalance, + StorageBalanceBounds, + StorageManagement, + FungibleTokenCore, + FungibleTokenResolver, + FungibleToken, + FungibleTokenMetadata, +} from "near-contract-standards/lib"; //TODO: delete lib + +import { + AccountId, + Balance, + PromiseOrValue, + call, + view, + initialize, + NearBindgen, + IntoStorageKey, + near, +} from "near-sdk-js"; + +import { + Option, +} from "near-contract-standards/lib/non_fungible_token/utils"; // TODO: fix import + +class FTPrefix implements IntoStorageKey { + into_storage_key(): string { + return "A"; // TODO: What is the best value to put here? + } +} + +/** Implementation of a FungibleToken standard + * Allows to include NEP-141 compatible token to any contract. + * There are next traits that any contract may implement: + * - FungibleTokenCore -- interface with ft_transfer methods. FungibleToken provides methods for it. + * - FungibleTokenMetaData -- return metadata for the token in NEP-148, up to contract to implement. + * - StorageManager -- interface for NEP-145 for allocating storage per account. FungibleToken provides methods for it. + * - AccountRegistrar -- interface for an account to register and unregister + * + * For example usage, see examples/fungible-token/src/lib.rs. + */ +@NearBindgen({ requireInit: true }) +export class MyFt implements FungibleTokenCore, StorageManagement, FungibleTokenResolver { + token: FungibleToken; + metadata: FungibleTokenMetadata; + + constructor() { + this.token = new FungibleToken(); + this.metadata = new FungibleTokenMetadata("", "", "", "", null, null, 0); + } + + @initialize({}) + init({ + owner_id, + total_supply, + metadata, + }: { + owner_id: AccountId; + total_supply: Balance; + metadata: FungibleTokenMetadata; + }) { + metadata.assert_valid(); + this.token = new FungibleToken().init(new FTPrefix()); + this.metadata = metadata; + this.token.internal_register_account(owner_id); + this.token.internal_deposit(owner_id, total_supply); + } + + @initialize({}) + init_with_default_meta({ + owner_id, + total_supply + }: { + owner_id: AccountId; + total_supply: Balance; + }) { + const metadata = new FungibleTokenMetadata( + "ft-1.0.0", + "Example NEAR fungible token", + "EXAMPLE", + "DATA_IMAGE_SVG_NEAR_ICON", + null, + null, + 24, + ); + return this.init({ + owner_id, + total_supply, + metadata + }) + } + + + @call({}) + measure_account_storage_usage() { + return this.token.measure_account_storage_usage(); + } + + /** Implementation of FungibleTokenCore */ + @call({ payableFunction: true }) + ft_transfer({ + receiver_id, + amount, + memo + }: { + receiver_id: AccountId, + amount: Balance, + memo?: String + }) { + return this.token.ft_transfer({ receiver_id, amount, memo }); + } + + @call({ payableFunction: true }) + ft_transfer_call({ + receiver_id, + amount, + memo, + msg + }: { + receiver_id: AccountId, + amount: Balance, + memo: Option, + msg: string + }): PromiseOrValue { + return this.token.ft_transfer_call({ receiver_id, amount, memo, msg }); + } + + @view({}) + ft_total_supply(): Balance { + return this.token.ft_total_supply(); + } + + @view({}) + ft_balance_of({ account_id }: { account_id: AccountId }): Balance { + return this.token.ft_balance_of({ account_id }); + } + + /** Implementation of StorageManagement + * @param registration_only doesn't affect the implementation for vanilla fungible token. + */ + @call({ payableFunction: true }) + storage_deposit( + { + account_id, + registration_only, + }: { + account_id?: AccountId, + registration_only?: boolean, + } + ): StorageBalance { + return this.token.storage_deposit({ account_id, registration_only }); + } + + /** + * While storage_withdraw normally allows the caller to retrieve `available` balance, the basic + * Fungible Token implementation sets storage_balance_bounds.min == storage_balance_bounds.max, + * which means available balance will always be 0. So this implementation: + * - panics if `amount > 0` + * - never transfers Ⓝ to caller + * - returns a `storage_balance` struct if `amount` is 0 + */ + @view({}) + storage_withdraw({ amount }: { amount?: bigint }): StorageBalance { + return this.token.storage_withdraw({ amount }); + } + + @call({ payableFunction: true }) + storage_unregister({ force }: { force?: boolean }): boolean { + return this.token.storage_unregister({ force }); + } + + @view({}) + storage_balance_bounds(): StorageBalanceBounds { + return this.token.storage_balance_bounds(); + } + + @view({}) + storage_balance_of({ account_id }: { account_id: AccountId }): Option { + return this.token.storage_balance_of({ account_id }); + } + + @call({}) + ft_resolve_transfer({ + sender_id, + receiver_id, + amount + }: { + sender_id: AccountId, + receiver_id: AccountId, + amount: Balance + }): Balance { + return this.token.ft_resolve_transfer({ sender_id, receiver_id, amount }); + } +} diff --git a/examples/src/standard-nft/my-nft.ts b/examples/src/standard-nft/my-nft.ts index e19103f03..d01b3178a 100644 --- a/examples/src/standard-nft/my-nft.ts +++ b/examples/src/standard-nft/my-nft.ts @@ -28,7 +28,7 @@ import { NonFungibleTokenResolver } from "near-contract-standards/lib/non_fungib import { NonFungibleTokenApproval } from "near-contract-standards/lib/non_fungible_token/approval"; import { NonFungibleTokenEnumeration } from "near-contract-standards/lib/non_fungible_token/enumeration"; -class StorageKey {} +class StorageKey { } class StorageKeyNonFungibleToken extends StorageKey implements IntoStorageKey { into_storage_key(): string { @@ -57,12 +57,11 @@ class StorageKeyApproval extends StorageKey implements IntoStorageKey { @NearBindgen({ requireInit: true }) export class MyNFT implements - NonFungibleTokenCore, - NonFungibleTokenMetadataProvider, - NonFungibleTokenResolver, - NonFungibleTokenApproval, - NonFungibleTokenEnumeration -{ + NonFungibleTokenCore, + NonFungibleTokenMetadataProvider, + NonFungibleTokenResolver, + NonFungibleTokenApproval, + NonFungibleTokenEnumeration { tokens: NonFungibleToken; metadata: Option; diff --git a/packages/near-contract-standards/README.md b/packages/near-contract-standards/README.md new file mode 100644 index 000000000..b00dd99bf --- /dev/null +++ b/packages/near-contract-standards/README.md @@ -0,0 +1,5 @@ +# Package for NEAR JS contract standards + +This package provides a set of interfaces and implementations for NEAR's contract standards: + - NFT + - FT (NEP-141) diff --git a/packages/near-contract-standards/lib/fungible_token/core.d.ts b/packages/near-contract-standards/lib/fungible_token/core.d.ts new file mode 100644 index 000000000..1975aa60b --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/core.d.ts @@ -0,0 +1,58 @@ +import { AccountId, PromiseOrValue, Balance } from "near-sdk-js"; +import { Option } from "../non_fungible_token/utils"; +export interface FungibleTokenCore { + /** + * Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`. + * Both accounts must be registered with the contract for transfer to succeed. (See [NEP-145](https://github.com/near/NEPs/discussions/145)) + * This method must to be able to accept attached deposits, and must not panic on attached deposit. + * Exactly 1 yoctoNEAR must be attached. + * See [the Security section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Arguments: + * @param receiver_id - the account ID of the receiver. + * @param amount - the amount of tokens to transfer. Must be a positive number in decimal string representation. + * @param memo - an optional string field in a free form to associate a memo with this transfer. + */ + ft_transfer({ receiver_id, amount, memo }: { + receiver_id: AccountId; + amount: Balance; + memo?: String; + }): any; + /** + * Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id` account. Then + * calls `ft_on_transfer` method on `receiver_id` contract and attaches a callback to resolve this transfer. + * `ft_on_transfer` method must return the amount of tokens unused by the receiver contract, the remaining tokens + * must be refunded to the `predecessor_account_id` at the resolve transfer callback. + * + * Token contract must pass all the remaining unused gas to the `ft_on_transfer` call. + * + * Malicious or invalid behavior by the receiver's contract: + * - If the receiver contract promise fails or returns invalid value, the full transfer amount must be refunded. + * - If the receiver contract overspent the tokens, and the `receiver_id` balance is lower than the required refund + * amount, the remaining balance must be refunded. See [the Security section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Both accounts must be registered with the contract for transfer to succeed. (See #145) + * This method must to be able to accept attached deposits, and must not panic on attached deposit. Exactly 1 yoctoNEAR must be attached. See [the Security + * section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Arguments: + * @param receiver_id - the account ID of the receiver contract. This contract will be called. + * @param amount - the amount of tokens to transfer. Must be a positive number in a decimal string representation. + * @param memo - an optional string field in a free form to associate a memo with this transfer. + * @param msg - a string message that will be passed to `ft_on_transfer` contract call. + * + * @returns a promise which will result in the amount of tokens withdrawn from sender's account. + */ + ft_transfer_call({ receiver_id, amount, memo, msg }: { + receiver_id: AccountId; + amount: Balance; + memo: Option; + msg: String; + }): PromiseOrValue; + /** Returns the total supply of the token in a decimal string representation. */ + ft_total_supply(): Balance; + /** Returns the balance of the account. If the account doesn't exist must returns `"0"`. */ + ft_balance_of({ account_id }: { + account_id: AccountId; + }): Balance; +} diff --git a/packages/near-contract-standards/lib/fungible_token/core.js b/packages/near-contract-standards/lib/fungible_token/core.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/core.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/near-contract-standards/lib/fungible_token/core_impl.d.ts b/packages/near-contract-standards/lib/fungible_token/core_impl.d.ts new file mode 100644 index 000000000..f45d83bd2 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/core_impl.d.ts @@ -0,0 +1,89 @@ +import { StorageBalance, StorageBalanceBounds, StorageManagement } from "../storage_management"; +import { FungibleTokenCore } from "./core"; +import { FungibleTokenResolver } from "./resolver"; +import { AccountId, LookupMap, Balance, PromiseOrValue, StorageUsage, IntoStorageKey } from "near-sdk-js"; +import { Option } from '../non_fungible_token/utils'; +/** Implementation of a FungibleToken standard + * Allows to include NEP-141 compatible token to any contract. + * There are next traits that any contract may implement: + * - FungibleTokenCore -- interface with ft_transfer methods. FungibleToken provides methods for it. + * - FungibleTokenMetaData -- return metadata for the token in NEP-148, up to contract to implement. + * - StorageManager -- interface for NEP-145 for allocating storage per account. FungibleToken provides methods for it. + * - AccountRegistrar -- interface for an account to register and unregister + * + * For example usage, see examples/fungible-token/src/lib.rs. + */ +export declare class FungibleToken implements FungibleTokenCore, StorageManagement, FungibleTokenResolver { + accounts: LookupMap; + total_supply: Balance; + account_storage_usage: StorageUsage; + constructor(); + init(prefix: IntoStorageKey): this; + measure_account_storage_usage(): void; + internal_unwrap_balance_of(account_id: AccountId): Balance; + internal_deposit(account_id: AccountId, amount: Balance): void; + internal_withdraw(account_id: AccountId, amount: Balance): void; + internal_transfer(sender_id: AccountId, receiver_id: AccountId, amount: Balance, memo?: String): void; + internal_register_account(account_id: AccountId): void; + /** Internal method that returns the amount of burned tokens in a corner case when the sender + * has deleted (unregistered) their account while the `ft_transfer_call` was still in flight. + * Returns (Used token amount, Burned token amount) + */ + internal_ft_resolve_transfer(sender_id: AccountId, receiver_id: AccountId, amount: Balance): [bigint, bigint]; + /** Implementation of FungibleTokenCore */ + ft_transfer({ receiver_id, amount, memo }: { + receiver_id: AccountId; + amount: Balance; + memo?: String; + }): void; + ft_transfer_call({ receiver_id, amount, memo, msg }: { + receiver_id: AccountId; + amount: Balance; + memo: Option; + msg: string; + }): PromiseOrValue; + ft_total_supply(): Balance; + ft_balance_of({ account_id }: { + account_id: AccountId; + }): Balance; + /** Implementation of storage + * Internal method that returns the Account ID and the balance in case the account was + * unregistered. + */ + internal_storage_unregister(force?: boolean): Option<[AccountId, Balance]>; + internal_storage_balance_of(account_id: AccountId): Option; + /** Implementation of StorageManagement + * @param registration_only doesn't affect the implementation for vanilla fungible token. + */ + storage_deposit({ account_id, registration_only, }: { + account_id?: AccountId; + registration_only?: boolean; + }): StorageBalance; + /** + * While storage_withdraw normally allows the caller to retrieve `available` balance, the basic + * Fungible Token implementation sets storage_balance_bounds.min == storage_balance_bounds.max, + * which means available balance will always be 0. So this implementation: + * - panics if `amount > 0` + * - never transfers Ⓝ to caller + * - returns a `storage_balance` struct if `amount` is 0 + */ + storage_withdraw({ amount }: { + amount?: bigint; + }): StorageBalance; + storage_unregister({ force }: { + force?: boolean; + }): boolean; + storage_balance_bounds(): StorageBalanceBounds; + storage_balance_of({ account_id }: { + account_id: AccountId; + }): Option; + /** Implementation of FungibleTokenResolver */ + ft_resolve_transfer({ sender_id, receiver_id, amount }: { + sender_id: AccountId; + receiver_id: AccountId; + amount: Balance; + }): Balance; + bigIntMax: (...args: bigint[]) => bigint; + bigIntMin: (...args: bigint[]) => bigint; + static reconstruct(data: FungibleToken): FungibleToken; +} diff --git a/packages/near-contract-standards/lib/fungible_token/core_impl.js b/packages/near-contract-standards/lib/fungible_token/core_impl.js new file mode 100644 index 000000000..ead56ffa7 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/core_impl.js @@ -0,0 +1,276 @@ +import { StorageBalance, StorageBalanceBounds } from "../storage_management"; +import { FtBurn, FtTransfer } from "./events"; +import { near, LookupMap, NearPromise, assert, } from "near-sdk-js"; +// TODO: move to the main SDK package +import { assert_one_yocto } from "../non_fungible_token/utils"; +const GAS_FOR_RESOLVE_TRANSFER = 15000000000000n; +const GAS_FOR_FT_TRANSFER_CALL = 25000000000000n + GAS_FOR_RESOLVE_TRANSFER; +const ERR_TOTAL_SUPPLY_OVERFLOW = "Total supply overflow"; +/** Implementation of a FungibleToken standard + * Allows to include NEP-141 compatible token to any contract. + * There are next traits that any contract may implement: + * - FungibleTokenCore -- interface with ft_transfer methods. FungibleToken provides methods for it. + * - FungibleTokenMetaData -- return metadata for the token in NEP-148, up to contract to implement. + * - StorageManager -- interface for NEP-145 for allocating storage per account. FungibleToken provides methods for it. + * - AccountRegistrar -- interface for an account to register and unregister + * + * For example usage, see examples/fungible-token/src/lib.rs. + */ +export class FungibleToken { + constructor() { + this.bigIntMax = (...args) => args.reduce((m, e) => e > m ? e : m); + this.bigIntMin = (...args) => args.reduce((m, e) => e < m ? e : m); + this.accounts = new LookupMap(""); + this.total_supply = 0n; + this.account_storage_usage = 0n; + } + init(prefix) { + const storage_prefix = prefix.into_storage_key(); + this.accounts = new LookupMap(storage_prefix); + this.total_supply = 0n; + this.account_storage_usage = 0n; + this.measure_account_storage_usage(); + return this; + } + measure_account_storage_usage() { + let initial_storage_usage = near.storageUsage(); + let tmp_account_id = "a".repeat(64); + this.accounts.set(tmp_account_id, 0n); + this.account_storage_usage = near.storageUsage() - initial_storage_usage; + this.accounts.remove(tmp_account_id); + } + internal_unwrap_balance_of(account_id) { + let balance = this.accounts.get(account_id); + if (balance === null) { + throw Error(`The account ${account_id} is not registered`); + } + return BigInt(balance); + } + internal_deposit(account_id, amount) { + let balance = BigInt(this.internal_unwrap_balance_of(account_id)); + let new_balance = balance + amount; + this.accounts.set(account_id, new_balance); + let new_total_supply = this.total_supply + amount; + this.total_supply = new_total_supply; + } + internal_withdraw(account_id, amount) { + let balance = BigInt(this.internal_unwrap_balance_of(account_id)); + let new_balance = balance - amount; + if (new_balance < 0) { + throw Error("The account doesn't have enough balance"); + } + this.accounts.set(account_id, new_balance); + let new_total_supply = this.total_supply - amount; + this.total_supply = new_total_supply; + } + internal_transfer(sender_id, receiver_id, amount, memo) { + assert(sender_id != receiver_id, "Sender and receiver should be different"); + assert(amount > 0, "The amount should be a positive number"); + this.internal_withdraw(sender_id, amount); + this.internal_deposit(receiver_id, amount); + new FtTransfer(sender_id, receiver_id, amount, memo).emit(); + } + internal_register_account(account_id) { + if (this.accounts.containsKey(account_id)) { + throw Error("The account is already registered"); + } + this.accounts.set(account_id, 0n); + } + /** Internal method that returns the amount of burned tokens in a corner case when the sender + * has deleted (unregistered) their account while the `ft_transfer_call` was still in flight. + * Returns (Used token amount, Burned token amount) + */ + internal_ft_resolve_transfer(sender_id, receiver_id, amount) { + // Get the unused amount from the `ft_on_transfer` call result. + let unused_amount; + try { + const promise_result = near.promiseResult(0).replace(/"*/g, ''); //TODO: why promiseResult returnes result with brackets? + unused_amount = this.bigIntMin(amount, BigInt(promise_result)); + } + catch (e) { + if (e.message.includes('Failed')) { + unused_amount = amount; + } + else { + throw e; + } + } + if (unused_amount > 0) { + let receiver_balance = BigInt(this.accounts.get(receiver_id) ?? 0); + if (receiver_balance > 0n) { + let refund_amount = this.bigIntMin(receiver_balance, unused_amount); + let new_receiver_balance = receiver_balance - refund_amount; + if (new_receiver_balance < 0n) { + throw Error("The receiver account doesn't have enough balance"); + } + this.accounts.set(receiver_id, new_receiver_balance); + let sender_balance = this.accounts.get(sender_id); + if (sender_balance) { + sender_balance = BigInt(sender_balance); + let new_sender_balance = sender_balance + refund_amount; + this.accounts.set(sender_id, new_sender_balance); + new FtTransfer(receiver_id, sender_id, refund_amount, "refund").emit(); + let used_amount = amount - refund_amount; + if (used_amount < 0n) { + throw Error(ERR_TOTAL_SUPPLY_OVERFLOW); + } + return [used_amount.valueOf(), 0n]; + } + else { + const new_total_supply = this.total_supply - refund_amount; + if (new_total_supply < 0) { + throw Error(ERR_TOTAL_SUPPLY_OVERFLOW); + } + this.total_supply = new_total_supply; + near.log("The account of the sender was deleted"); + new FtBurn(receiver_id, refund_amount, "refund").emit(); + return [amount, refund_amount]; + } + } + } + return [amount, 0n]; + } + /** Implementation of FungibleTokenCore */ + ft_transfer({ receiver_id, amount, memo }) { + amount = BigInt(amount); + assert_one_yocto(); + let sender_id = near.predecessorAccountId(); + this.internal_transfer(sender_id, receiver_id, amount, memo); + } + ft_transfer_call({ receiver_id, amount, memo, msg }) { + amount = BigInt(amount); + assert_one_yocto(); + assert(near.prepaidGas() > GAS_FOR_FT_TRANSFER_CALL, "More gas is required"); + let sender_id = near.predecessorAccountId(); + this.internal_transfer(sender_id, receiver_id, amount, memo); + let receiver_gas = near.prepaidGas() - GAS_FOR_FT_TRANSFER_CALL; + if (receiver_gas < 0) { + throw new Error("Prepaid gas overflow"); + } + return NearPromise.new(receiver_id) + .functionCall("ft_on_transfer", JSON.stringify({ sender_id, amount: String(amount), msg }), BigInt(0), receiver_gas) + .then(NearPromise.new(near.currentAccountId()) + .functionCall("ft_resolve_transfer", JSON.stringify({ sender_id, receiver_id, amount: String(amount) }), BigInt(0), GAS_FOR_RESOLVE_TRANSFER)); + } + ft_total_supply() { + return this.total_supply; + } + ft_balance_of({ account_id }) { + return this.accounts.get(account_id) ?? 0n; + } + /** Implementation of storage + * Internal method that returns the Account ID and the balance in case the account was + * unregistered. + */ + internal_storage_unregister(force) { + assert_one_yocto(); + let account_id = near.predecessorAccountId(); + let balance = BigInt(this.accounts.get(account_id)); + if (balance || balance == 0n) { + if (balance == 0n || force) { + this.accounts.remove(account_id); + this.total_supply = this.total_supply - balance; + NearPromise.new(account_id).transfer(this.storage_balance_bounds().min + 1n); + return [account_id, balance]; + } + else { + throw Error("Can't unregister the account with the positive balance without force"); + } + } + else { + near.log(`The account ${account_id} is not registered`); + return null; + } + } + internal_storage_balance_of(account_id) { + if (this.accounts.containsKey(account_id)) { + return new StorageBalance(this.storage_balance_bounds().min, 0n); + } + else { + return null; + } + } + /** Implementation of StorageManagement + * @param registration_only doesn't affect the implementation for vanilla fungible token. + */ + storage_deposit({ account_id, registration_only, }) { + let amount = near.attachedDeposit(); + account_id = account_id ?? near.predecessorAccountId(); + if (this.accounts.containsKey(account_id)) { + near.log("The account is already registered, refunding the deposit"); + if (amount > 0) { + NearPromise.new(near.predecessorAccountId()).transfer(amount); + } + } + else { + let min_balance = this.storage_balance_bounds().min; + if (amount < min_balance) { + throw Error("The attached deposit is less than the minimum storage balance"); + } + this.internal_register_account(account_id); + let refund = amount - min_balance; + if (refund > 0) { + NearPromise.new(near.predecessorAccountId()).transfer(refund); + } + } + return this.internal_storage_balance_of(account_id); + } + /** + * While storage_withdraw normally allows the caller to retrieve `available` balance, the basic + * Fungible Token implementation sets storage_balance_bounds.min == storage_balance_bounds.max, + * which means available balance will always be 0. So this implementation: + * - panics if `amount > 0` + * - never transfers Ⓝ to caller + * - returns a `storage_balance` struct if `amount` is 0 + */ + storage_withdraw({ amount }) { + amount = BigInt(amount); + assert_one_yocto(); + let predecessor_account_id = near.predecessorAccountId(); + const storage_balance = this.internal_storage_balance_of(predecessor_account_id); + if (storage_balance) { + if (amount && amount > 0) { + throw Error("The amount is greater than the available storage balance"); + } + return storage_balance; + } + else { + throw Error(`The account ${predecessor_account_id} is not registered`); + } + } + storage_unregister({ force }) { + return this.internal_storage_unregister(force) ? true : false; + } + storage_balance_bounds() { + let required_storage_balance = this.account_storage_usage * near.storageByteCost(); + return new StorageBalanceBounds(required_storage_balance, required_storage_balance); + } + storage_balance_of({ account_id }) { + return this.internal_storage_balance_of(account_id); + } + /** Implementation of FungibleTokenResolver */ + ft_resolve_transfer({ sender_id, receiver_id, amount }) { + amount = BigInt(amount); + const res = this.internal_ft_resolve_transfer(sender_id, receiver_id, amount); + const used_amount = res[0]; + const burned_amount = res[1]; + if (burned_amount > 0) { + near.log(`Account @${sender_id} burned ${burned_amount}`); + } + return used_amount; + } + static reconstruct(data) { + const ret = new FungibleToken(); + Object.assign(ret, data); + if (ret.accounts) { + ret.accounts = LookupMap.reconstruct(ret.accounts); + } + if (ret.total_supply) { + ret.total_supply = BigInt(ret.total_supply); + } + if (ret.account_storage_usage) { + ret.account_storage_usage = BigInt(ret.account_storage_usage); + } + return ret; + } +} diff --git a/packages/near-contract-standards/lib/fungible_token/events.d.ts b/packages/near-contract-standards/lib/fungible_token/events.d.ts new file mode 100644 index 000000000..8da7e4b9e --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/events.d.ts @@ -0,0 +1,73 @@ +/** + * Standard for nep141 (Fungible Token) events. + * + * These events will be picked up by the NEAR indexer. + * + * + * + * This is an extension of the events format (nep-297): + * + * + * The three events in this standard are [`FtMint`], [`FtTransfer`], and [`FtBurn`]. + * + * These events can be logged by calling `.emit()` on them if a single event, or calling + * [`FtMint::emit_many`], [`FtTransfer::emit_many`], + * or [`FtBurn::emit_many`] respectively. + */ +import { NearEvent } from "../event"; +import { Option } from "../non_fungible_token/utils"; +import { AccountId, Balance } from "near-sdk-js"; +export type Nep141EventKind = FtMint[] | FtTransfer[] | FtBurn[]; +export declare class Nep141Event extends NearEvent { + version: string; + event_kind: Nep141EventKind; + constructor(version: string, event_kind: Nep141EventKind); +} +/** Data to log for an FT mint event. To log this event, call [`.emit()`](FtMint::emit). */ +export declare class FtMint { + owner_id: AccountId; + amount: number; + memo: Option; + constructor(owner_id: AccountId, amount: number, memo: Option); + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit(): void; + /** Emits an FT mint event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtMint`] represents the data of each mint. + */ + emit_many(data: FtMint[]): void; +} +/** Data to log for an FT transfer event. To log this event, + * call [`.emit()`](FtTransfer::emit). + */ +export declare class FtTransfer { + old_owner_id: AccountId; + new_owner_id: AccountId; + amount: string; + memo: Option; + constructor(old_owner_id: AccountId, new_owner_id: AccountId, amount: bigint, memo: Option); + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit(): void; + /** Emits an FT transfer event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtTransfer`] represents the data of each transfer. + */ + emit_many(data: FtTransfer[]): void; +} +/** Data to log for an FT burn event. To log this event, call [`.emit()`](FtBurn::emit). */ +export declare class FtBurn { + owner_id: AccountId; + amount: string; + memo: Option; + constructor(owner_id: AccountId, amount: Balance, memo: Option); + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit(): void; + /** Emits an FT burn event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtBurn`] represents the data of each burn. + */ + emit_many(data: FtBurn[]): void; +} diff --git a/packages/near-contract-standards/lib/fungible_token/events.js b/packages/near-contract-standards/lib/fungible_token/events.js new file mode 100644 index 000000000..512f72c95 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/events.js @@ -0,0 +1,93 @@ +/** + * Standard for nep141 (Fungible Token) events. + * + * These events will be picked up by the NEAR indexer. + * + * + * + * This is an extension of the events format (nep-297): + * + * + * The three events in this standard are [`FtMint`], [`FtTransfer`], and [`FtBurn`]. + * + * These events can be logged by calling `.emit()` on them if a single event, or calling + * [`FtMint::emit_many`], [`FtTransfer::emit_many`], + * or [`FtBurn::emit_many`] respectively. + */ +import { NearEvent } from "../event"; +export class Nep141Event extends NearEvent { + constructor(version, event_kind) { + super(); + this.version = version; + this.event_kind = event_kind; + } +} +/** Data to log for an FT mint event. To log this event, call [`.emit()`](FtMint::emit). */ +export class FtMint { + constructor(owner_id, amount, memo) { + this.owner_id = owner_id; + this.amount = amount; + this.memo = memo; + } + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]); + } + /** Emits an FT mint event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtMint`] represents the data of each mint. + */ + emit_many(data) { + new_141_v1(data).emit(); + } +} +/** Data to log for an FT transfer event. To log this event, + * call [`.emit()`](FtTransfer::emit). + */ +export class FtTransfer { + constructor(old_owner_id, new_owner_id, amount, memo) { + this.old_owner_id = old_owner_id; + this.new_owner_id = new_owner_id; + this.amount = amount.toString(); + this.memo = memo; + } + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]); + } + /** Emits an FT transfer event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtTransfer`] represents the data of each transfer. + */ + emit_many(data) { + new_141_v1(data).emit(); + } +} +/** Data to log for an FT burn event. To log this event, call [`.emit()`](FtBurn::emit). */ +export class FtBurn { + constructor(owner_id, amount, memo) { + this.owner_id = owner_id; + this.amount = amount.toString(); + this.memo = memo; + } + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]); + } + /** Emits an FT burn event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtBurn`] represents the data of each burn. + */ + emit_many(data) { + new_141_v1(data).emit(); + } +} +function new_141(version, event_kind) { + return new Nep141Event(version, event_kind); +} +function new_141_v1(event_kind) { + return new_141("1.0.0", event_kind); +} diff --git a/packages/near-contract-standards/lib/fungible_token/index.d.ts b/packages/near-contract-standards/lib/fungible_token/index.d.ts new file mode 100644 index 000000000..101cc64cf --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/index.d.ts @@ -0,0 +1,6 @@ +export * from './core_impl'; +export * from './core'; +export * from './events'; +export * from './metadata'; +export * from './receiver'; +export * from './resolver'; diff --git a/packages/near-contract-standards/lib/fungible_token/index.js b/packages/near-contract-standards/lib/fungible_token/index.js new file mode 100644 index 000000000..101cc64cf --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/index.js @@ -0,0 +1,6 @@ +export * from './core_impl'; +export * from './core'; +export * from './events'; +export * from './metadata'; +export * from './receiver'; +export * from './resolver'; diff --git a/packages/near-contract-standards/lib/fungible_token/metadata.d.ts b/packages/near-contract-standards/lib/fungible_token/metadata.d.ts new file mode 100644 index 000000000..5db30b4cd --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/metadata.d.ts @@ -0,0 +1,15 @@ +import { Option } from "../non_fungible_token/utils"; +export declare class FungibleTokenMetadata { + spec: string; + name: string; + symbol: string; + icon: Option; + reference: Option; + reference_hash: Option; + decimals: number; + constructor(spec: string, name: string, symbol: string, icon: Option, referance: Option, referance_hash: Option, decimals: number); + assert_valid(): void; +} +export interface FungibleTokenMetadataProvider { + ft_metadata(): FungibleTokenMetadata; +} diff --git a/packages/near-contract-standards/lib/fungible_token/metadata.js b/packages/near-contract-standards/lib/fungible_token/metadata.js new file mode 100644 index 000000000..82be61985 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/metadata.js @@ -0,0 +1,22 @@ +import { assert, } from "near-sdk-js"; +const FT_METADATA_SPEC = "ft-1.0.0"; +export class FungibleTokenMetadata { + constructor(spec, name, symbol, icon, referance, referance_hash, decimals) { + this.spec = spec; + this.name = name; + this.symbol = symbol; + this.icon = icon; + this.reference = referance; + this.reference_hash = referance_hash; + this.decimals = decimals; + } + assert_valid() { + assert(this.spec == FT_METADATA_SPEC, "Invalid FT_METADATA_SPEC"); + const isReferenceProvided = this.reference ? true : false; + const isReferenceHashProvided = this.reference_hash ? true : false; + assert(isReferenceHashProvided === isReferenceProvided, "reference and reference_hash must be either both provided or not"); + if (this.reference_hash) { + assert(this.reference_hash.length === 32, "reference_hash must be 32 bytes"); + } + } +} diff --git a/packages/near-contract-standards/lib/fungible_token/receiver.d.ts b/packages/near-contract-standards/lib/fungible_token/receiver.d.ts new file mode 100644 index 000000000..d63dfb404 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/receiver.d.ts @@ -0,0 +1,28 @@ +import { AccountId, PromiseOrValue } from "near-sdk-js"; +export interface FungibleTokenReceiver { + /** + * Called by fungible token contract after `ft_transfer_call` was initiated by + * `sender_id` of the given `amount` with the transfer message given in `msg` field. + * The `amount` of tokens were already transferred to this contract account and ready to be used. + * + * The method must return the amount of tokens that are *not* used/accepted by this contract from the transferred + * amount. Examples: + * - The transferred amount was `500`, the contract completely takes it and must return `0`. + * - The transferred amount was `500`, but this transfer call only needs `450` for the action passed in the `msg` + * field, then the method must return `50`. + * - The transferred amount was `500`, but the action in `msg` field has expired and the transfer must be + * cancelled. The method must return `500` or panic. + * + * Arguments: + * @param sender_id - the account ID that initiated the transfer. + * @param amount - the amount of tokens that were transferred to this account in a decimal string representation. + * @param msg - a string message that was passed with this transfer call. + * + * @returns the amount of unused tokens that should be returned to sender, in a decimal string representation. + */ + ft_on_transfer({ sender_id, amount, msg }: { + sender_id: AccountId; + amount: number; + msg: String; + }): PromiseOrValue; +} diff --git a/packages/near-contract-standards/lib/fungible_token/receiver.js b/packages/near-contract-standards/lib/fungible_token/receiver.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/receiver.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/near-contract-standards/lib/fungible_token/resolver.d.ts b/packages/near-contract-standards/lib/fungible_token/resolver.d.ts new file mode 100644 index 000000000..f8d5255d9 --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/resolver.d.ts @@ -0,0 +1,8 @@ +import { AccountId, Balance } from "near-sdk-js"; +export interface FungibleTokenResolver { + ft_resolve_transfer({ sender_id, receiver_id, amount, }: { + sender_id: AccountId; + receiver_id: AccountId; + amount: Balance; + }): Balance; +} diff --git a/packages/near-contract-standards/lib/fungible_token/resolver.js b/packages/near-contract-standards/lib/fungible_token/resolver.js new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/packages/near-contract-standards/lib/fungible_token/resolver.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/near-contract-standards/lib/index.d.ts b/packages/near-contract-standards/lib/index.d.ts index 20839e61e..ccdd4d9a3 100644 --- a/packages/near-contract-standards/lib/index.d.ts +++ b/packages/near-contract-standards/lib/index.d.ts @@ -1,2 +1,5 @@ -/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken/README.html). */ +/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken). */ export * from "./non_fungible_token"; +/** Fungible tokens as described in [by the spec](https://nomicon.io/Standards/FungibleToken). */ +export * from "./fungible_token"; +export * from "./storage_management"; diff --git a/packages/near-contract-standards/lib/index.js b/packages/near-contract-standards/lib/index.js index 20839e61e..ccdd4d9a3 100644 --- a/packages/near-contract-standards/lib/index.js +++ b/packages/near-contract-standards/lib/index.js @@ -1,2 +1,5 @@ -/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken/README.html). */ +/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken). */ export * from "./non_fungible_token"; +/** Fungible tokens as described in [by the spec](https://nomicon.io/Standards/FungibleToken). */ +export * from "./fungible_token"; +export * from "./storage_management"; diff --git a/packages/near-contract-standards/lib/storage_management/index.d.ts b/packages/near-contract-standards/lib/storage_management/index.d.ts new file mode 100644 index 000000000..141e38851 --- /dev/null +++ b/packages/near-contract-standards/lib/storage_management/index.d.ts @@ -0,0 +1,58 @@ +import { AccountId, Balance } from "near-sdk-js"; +import { Option } from "../non_fungible_token/utils"; +export declare class StorageBalance { + total: Balance; + available: Balance; + constructor(total: Balance, available: Balance); +} +export declare class StorageBalanceBounds { + constructor(min: Balance, max: Option); + min: Balance; + max: Option; +} +export interface StorageManagement { + /** + * @param registration_only if `true` MUST refund above the minimum balance if the account didn't exist and + * refund full deposit if the account exists. + */ + storage_deposit({ account_id, registration_only }: { + account_id: Option; + registration_only: Option; + }): StorageBalance; + /** Withdraw specified amount of available Ⓝ for predecessor account. + * + * This method is safe to call. It MUST NOT remove data. + * + * @param amount is sent as a string representing an unsigned 128-bit integer. If + * omitted, contract MUST refund full `available` balance. If `amount` exceeds + * predecessor account's available balance, contract MUST panic. + * + * If predecessor account not registered, contract MUST panic. + * + * MUST require exactly 1 yoctoNEAR attached balance to prevent restricted + * function-call access-key call (UX wallet security) + * + * @returns the StorageBalance structure showing updated balances. + */ + storage_withdraw({ amount }: { + amount?: bigint; + }): StorageBalance; + /** Unregisters the predecessor account and returns the storage NEAR deposit back. + * + * If the predecessor account is not registered, the function MUST return `false` without panic. + * + * @param force If `force=true` the function SHOULD ignore account balances (burn them) and close the account. + * Otherwise, MUST panic if caller has a positive registered balance (eg token holdings) or + * the contract doesn't support force unregistration. + * MUST require exactly 1 yoctoNEAR attached balance to prevent restricted function-call access-key call + * (UX wallet security) + * @returns `true` if the account was unregistered, `false` if account was not registered before. + */ + storage_unregister({ force }: { + force: Option; + }): boolean; + storage_balance_bounds(): StorageBalanceBounds; + storage_balance_of({ account_id }: { + account_id: AccountId; + }): Option; +} diff --git a/packages/near-contract-standards/lib/storage_management/index.js b/packages/near-contract-standards/lib/storage_management/index.js new file mode 100644 index 000000000..4a65b38db --- /dev/null +++ b/packages/near-contract-standards/lib/storage_management/index.js @@ -0,0 +1,12 @@ +export class StorageBalance { + constructor(total, available) { + this.total = total; + this.available = available; + } +} +export class StorageBalanceBounds { + constructor(min, max) { + this.min = min; + this.max = max; + } +} diff --git a/packages/near-contract-standards/src/event.ts b/packages/near-contract-standards/src/event.ts index 48753ee80..f37fb45f1 100644 --- a/packages/near-contract-standards/src/event.ts +++ b/packages/near-contract-standards/src/event.ts @@ -1,6 +1,7 @@ import { near } from "near-sdk-js"; export abstract class NearEvent { + private internal_to_json_string(): string { return JSON.stringify(this); } diff --git a/packages/near-contract-standards/src/fungible_token/core.ts b/packages/near-contract-standards/src/fungible_token/core.ts new file mode 100644 index 000000000..59d57d902 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/core.ts @@ -0,0 +1,72 @@ +import { AccountId, PromiseOrValue, Balance } from "near-sdk-js" +import { Option } from "../non_fungible_token/utils" +export interface FungibleTokenCore { + /** + * Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id`. + * Both accounts must be registered with the contract for transfer to succeed. (See [NEP-145](https://github.com/near/NEPs/discussions/145)) + * This method must to be able to accept attached deposits, and must not panic on attached deposit. + * Exactly 1 yoctoNEAR must be attached. + * See [the Security section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Arguments: + * @param receiver_id - the account ID of the receiver. + * @param amount - the amount of tokens to transfer. Must be a positive number in decimal string representation. + * @param memo - an optional string field in a free form to associate a memo with this transfer. + */ + ft_transfer({ + receiver_id, + amount, + memo + }: { + receiver_id: AccountId, + amount: Balance, + memo?: String + }); + + /** + * Transfers positive `amount` of tokens from the `env::predecessor_account_id` to `receiver_id` account. Then + * calls `ft_on_transfer` method on `receiver_id` contract and attaches a callback to resolve this transfer. + * `ft_on_transfer` method must return the amount of tokens unused by the receiver contract, the remaining tokens + * must be refunded to the `predecessor_account_id` at the resolve transfer callback. + * + * Token contract must pass all the remaining unused gas to the `ft_on_transfer` call. + * + * Malicious or invalid behavior by the receiver's contract: + * - If the receiver contract promise fails or returns invalid value, the full transfer amount must be refunded. + * - If the receiver contract overspent the tokens, and the `receiver_id` balance is lower than the required refund + * amount, the remaining balance must be refunded. See [the Security section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Both accounts must be registered with the contract for transfer to succeed. (See #145) + * This method must to be able to accept attached deposits, and must not panic on attached deposit. Exactly 1 yoctoNEAR must be attached. See [the Security + * section](https://github.com/near/NEPs/issues/141#user-content-security) of the standard. + * + * Arguments: + * @param receiver_id - the account ID of the receiver contract. This contract will be called. + * @param amount - the amount of tokens to transfer. Must be a positive number in a decimal string representation. + * @param memo - an optional string field in a free form to associate a memo with this transfer. + * @param msg - a string message that will be passed to `ft_on_transfer` contract call. + * + * @returns a promise which will result in the amount of tokens withdrawn from sender's account. + */ + ft_transfer_call({ + receiver_id, + amount, + memo, + msg + }: { + receiver_id: AccountId, + amount: Balance, + memo: Option, + msg: String + }): PromiseOrValue; + + /** Returns the total supply of the token in a decimal string representation. */ + ft_total_supply(): Balance; + + /** Returns the balance of the account. If the account doesn't exist must returns `"0"`. */ + ft_balance_of({ + account_id + }: { + account_id: AccountId + }): Balance; +} diff --git a/packages/near-contract-standards/src/fungible_token/core_impl.ts b/packages/near-contract-standards/src/fungible_token/core_impl.ts new file mode 100644 index 000000000..592957325 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/core_impl.ts @@ -0,0 +1,381 @@ +import { StorageBalance, StorageBalanceBounds, StorageManagement } from "../storage_management"; +import { FungibleTokenCore } from "./core"; +import { FtBurn, FtTransfer } from "./events"; +import { FungibleTokenResolver } from "./resolver"; +import { + near, + AccountId, + LookupMap, + Balance, + Gas, + PromiseOrValue, + NearPromise, + StorageUsage, + assert, + IntoStorageKey, +} from "near-sdk-js"; + +import { Option } from '../non_fungible_token/utils'; + +// TODO: move to the main SDK package +import { assert_one_yocto } from "../non_fungible_token/utils"; + +const GAS_FOR_RESOLVE_TRANSFER: Gas = 15_000_000_000_000n; +const GAS_FOR_FT_TRANSFER_CALL: Gas = 25_000_000_000_000n + GAS_FOR_RESOLVE_TRANSFER; +const ERR_TOTAL_SUPPLY_OVERFLOW: string = "Total supply overflow"; + +/** Implementation of a FungibleToken standard + * Allows to include NEP-141 compatible token to any contract. + * There are next traits that any contract may implement: + * - FungibleTokenCore -- interface with ft_transfer methods. FungibleToken provides methods for it. + * - FungibleTokenMetaData -- return metadata for the token in NEP-148, up to contract to implement. + * - StorageManager -- interface for NEP-145 for allocating storage per account. FungibleToken provides methods for it. + * - AccountRegistrar -- interface for an account to register and unregister + * + * For example usage, see examples/fungible-token/src/lib.rs. + */ +export class FungibleToken implements FungibleTokenCore, StorageManagement, FungibleTokenResolver { + // AccountID -> Account balance. + accounts: LookupMap; + + // Total supply of the all token. + total_supply: Balance; + + // The storage size in bytes for one account. + account_storage_usage: StorageUsage; + + constructor() { + this.accounts = new LookupMap(""); + this.total_supply = 0n; + this.account_storage_usage = 0n; + } + + init(prefix: IntoStorageKey) { + const storage_prefix = prefix.into_storage_key(); + this.accounts = new LookupMap(storage_prefix); + this.total_supply = 0n; + this.account_storage_usage = 0n; + this.measure_account_storage_usage(); + return this; + } + + + measure_account_storage_usage() { + let initial_storage_usage: bigint = near.storageUsage(); + let tmp_account_id: string = "a".repeat(64); + this.accounts.set(tmp_account_id, 0n); + this.account_storage_usage = near.storageUsage() - initial_storage_usage; + this.accounts.remove(tmp_account_id); + } + + internal_unwrap_balance_of(account_id: AccountId): Balance { + let balance = this.accounts.get(account_id); + if (balance === null) { + throw Error(`The account ${account_id} is not registered`); + } + return BigInt(balance); + } + + internal_deposit(account_id: AccountId, amount: Balance) { + let balance: Balance = BigInt(this.internal_unwrap_balance_of(account_id)); + let new_balance: Balance = balance + amount; + this.accounts.set(account_id, new_balance); + let new_total_supply: Balance = this.total_supply + amount; + this.total_supply = new_total_supply; + } + + internal_withdraw(account_id: AccountId, amount: Balance) { + let balance: Balance = BigInt(this.internal_unwrap_balance_of(account_id)); + let new_balance: Balance = balance - amount; + if (new_balance < 0) { + throw Error("The account doesn't have enough balance"); + } + this.accounts.set(account_id, new_balance); + let new_total_supply: Balance = this.total_supply - amount; + this.total_supply = new_total_supply; + } + + internal_transfer( + sender_id: AccountId, + receiver_id: AccountId, + amount: Balance, + memo?: String, + ) { + assert(sender_id != receiver_id, "Sender and receiver should be different"); + assert(amount > 0, "The amount should be a positive number"); + this.internal_withdraw(sender_id, amount); + this.internal_deposit(receiver_id, amount); + new FtTransfer(sender_id, receiver_id, amount, memo).emit(); + } + + internal_register_account(account_id: AccountId) { + if (this.accounts.containsKey(account_id)) { + throw Error("The account is already registered"); + } + this.accounts.set(account_id, 0n); + } + + /** Internal method that returns the amount of burned tokens in a corner case when the sender + * has deleted (unregistered) their account while the `ft_transfer_call` was still in flight. + * Returns (Used token amount, Burned token amount) + */ + internal_ft_resolve_transfer(sender_id: AccountId, receiver_id: AccountId, amount: Balance): [bigint, bigint] { + // Get the unused amount from the `ft_on_transfer` call result. + let unused_amount: Balance; + try { + const promise_result = near.promiseResult(0).replace(/"*/g, ''); //TODO: why promiseResult returnes result with brackets? + unused_amount = this.bigIntMin(amount, BigInt(promise_result)); + } catch (e) { + if (e.message.includes('Failed')) { + unused_amount = amount; + } else { + throw e; + } + } + + if (unused_amount > 0) { + let receiver_balance: Balance = BigInt(this.accounts.get(receiver_id) ?? 0); + if (receiver_balance > 0n) { + let refund_amount: Balance = this.bigIntMin(receiver_balance, unused_amount); + let new_receiver_balance: Balance = receiver_balance - refund_amount; + if (new_receiver_balance < 0n) { + throw Error("The receiver account doesn't have enough balance"); + } + this.accounts.set(receiver_id, new_receiver_balance); + let sender_balance = this.accounts.get(sender_id); + if (sender_balance) { + sender_balance = BigInt(sender_balance); + let new_sender_balance: Balance = sender_balance + refund_amount; + this.accounts.set(sender_id, new_sender_balance); + new FtTransfer( + receiver_id, + sender_id, + refund_amount, + "refund", + ).emit(); + + let used_amount: Balance = amount - refund_amount; + if (used_amount < 0n) { + throw Error(ERR_TOTAL_SUPPLY_OVERFLOW); + } + return [used_amount.valueOf(), 0n]; + } else { + const new_total_supply = this.total_supply - refund_amount; + if (new_total_supply < 0) { + throw Error(ERR_TOTAL_SUPPLY_OVERFLOW); + } + this.total_supply = new_total_supply + near.log("The account of the sender was deleted"); + new FtBurn( + receiver_id, + refund_amount, + "refund", + ).emit(); + return [amount, refund_amount]; + } + } + } + return [amount, 0n]; + } + + /** Implementation of FungibleTokenCore */ + ft_transfer({ + receiver_id, + amount, + memo + }: { + receiver_id: AccountId, + amount: Balance, + memo?: String + }) { + amount = BigInt(amount); + assert_one_yocto(); + let sender_id: AccountId = near.predecessorAccountId(); + this.internal_transfer(sender_id, receiver_id, amount, memo); + } + + ft_transfer_call({ + receiver_id, + amount, + memo, + msg + }: { + receiver_id: AccountId, + amount: Balance, + memo: Option, + msg: string + }): PromiseOrValue { + amount = BigInt(amount); + assert_one_yocto(); + assert(near.prepaidGas() > GAS_FOR_FT_TRANSFER_CALL, "More gas is required"); + let sender_id: AccountId = near.predecessorAccountId(); + this.internal_transfer(sender_id, receiver_id, amount, memo); + let receiver_gas: bigint = near.prepaidGas() - GAS_FOR_FT_TRANSFER_CALL; + if (receiver_gas < 0) { + throw new Error("Prepaid gas overflow"); + } + + return NearPromise.new(receiver_id) + .functionCall("ft_on_transfer", JSON.stringify({ sender_id, amount: String(amount), msg }), BigInt(0), receiver_gas) + .then( + NearPromise.new(near.currentAccountId()) + .functionCall( + "ft_resolve_transfer", + JSON.stringify({ sender_id, receiver_id, amount: String(amount) }), + BigInt(0), + GAS_FOR_RESOLVE_TRANSFER + ) + ); + } + + ft_total_supply(): Balance { + return this.total_supply; + } + + ft_balance_of({ account_id }: { account_id: AccountId }): Balance { + return this.accounts.get(account_id) ?? 0n; + } + + /** Implementation of storage + * Internal method that returns the Account ID and the balance in case the account was + * unregistered. + */ + internal_storage_unregister(force?: boolean): Option<[AccountId, Balance]> { + assert_one_yocto(); + let account_id: AccountId = near.predecessorAccountId(); + + let balance: Balance = BigInt(this.accounts.get(account_id)); + if (balance || balance == 0n) { + if (balance == 0n || force) { + this.accounts.remove(account_id); + this.total_supply = this.total_supply - balance; + NearPromise.new(account_id).transfer(this.storage_balance_bounds().min + 1n); + return [account_id, balance]; + } else { + throw Error("Can't unregister the account with the positive balance without force"); + } + } else { + near.log(`The account ${account_id} is not registered`); + return null; + } + } + + internal_storage_balance_of(account_id: AccountId): Option { + if (this.accounts.containsKey(account_id)) { + return new StorageBalance(this.storage_balance_bounds().min, 0n); + } else { + return null; + } + } + + /** Implementation of StorageManagement + * @param registration_only doesn't affect the implementation for vanilla fungible token. + */ + storage_deposit( + { + account_id, + registration_only, + }: { + account_id?: AccountId, + registration_only?: boolean, + } + ): StorageBalance { + let amount: Balance = near.attachedDeposit(); + account_id = account_id ?? near.predecessorAccountId(); + if (this.accounts.containsKey(account_id)) { + near.log!("The account is already registered, refunding the deposit"); + if (amount > 0) { + NearPromise.new(near.predecessorAccountId()).transfer(amount); + } + } else { + let min_balance: Balance = this.storage_balance_bounds().min; + if (amount < min_balance) { + throw Error("The attached deposit is less than the minimum storage balance"); + } + + this.internal_register_account(account_id); + let refund: Balance = amount - min_balance; + if (refund > 0) { + NearPromise.new(near.predecessorAccountId()).transfer(refund); + } + } + return this.internal_storage_balance_of(account_id); + } + + /** + * While storage_withdraw normally allows the caller to retrieve `available` balance, the basic + * Fungible Token implementation sets storage_balance_bounds.min == storage_balance_bounds.max, + * which means available balance will always be 0. So this implementation: + * - panics if `amount > 0` + * - never transfers Ⓝ to caller + * - returns a `storage_balance` struct if `amount` is 0 + */ + storage_withdraw({ amount }: { amount?: bigint }): StorageBalance { + amount = BigInt(amount); + assert_one_yocto(); + let predecessor_account_id: AccountId = near.predecessorAccountId(); + const storage_balance = this.internal_storage_balance_of(predecessor_account_id); + if (storage_balance) { + if (amount && amount > 0) { + throw Error("The amount is greater than the available storage balance"); + } + return storage_balance; + } else { + throw Error(`The account ${predecessor_account_id} is not registered`) + } + } + + storage_unregister({ force }: { force?: boolean }): boolean { + return this.internal_storage_unregister(force) ? true : false; + } + + storage_balance_bounds(): StorageBalanceBounds { + let required_storage_balance: Balance = this.account_storage_usage * near.storageByteCost(); + return new StorageBalanceBounds(required_storage_balance, required_storage_balance); + } + + storage_balance_of({ account_id }: { account_id: AccountId }): Option { + return this.internal_storage_balance_of(account_id); + } + + /** Implementation of FungibleTokenResolver */ + ft_resolve_transfer({ + sender_id, + receiver_id, + amount + }: { + sender_id: AccountId, + receiver_id: AccountId, + amount: Balance + }): Balance { + amount = BigInt(amount); + const res = this.internal_ft_resolve_transfer(sender_id, receiver_id, amount); + const used_amount = res[0]; + const burned_amount = res[1]; + if (burned_amount > 0) { + near.log(`Account @${sender_id} burned ${burned_amount}`); + } + return used_amount; + } + + bigIntMax = (...args: bigint[]) => args.reduce((m, e) => e > m ? e : m); + bigIntMin = (...args: bigint[]) => args.reduce((m, e) => e < m ? e : m); + + static reconstruct(data: FungibleToken): FungibleToken { + const ret = new FungibleToken(); + Object.assign(ret, data); + if (ret.accounts) { + ret.accounts = LookupMap.reconstruct(ret.accounts); + } + + if (ret.total_supply) { + ret.total_supply = BigInt(ret.total_supply) as Balance; + } + + if (ret.account_storage_usage) { + ret.account_storage_usage = BigInt(ret.account_storage_usage) as StorageUsage; + } + + return ret; + } +} diff --git a/packages/near-contract-standards/src/fungible_token/events.ts b/packages/near-contract-standards/src/fungible_token/events.ts new file mode 100644 index 000000000..96a613c07 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/events.ts @@ -0,0 +1,127 @@ +/** + * Standard for nep141 (Fungible Token) events. + * + * These events will be picked up by the NEAR indexer. + * + * + * + * This is an extension of the events format (nep-297): + * + * + * The three events in this standard are [`FtMint`], [`FtTransfer`], and [`FtBurn`]. + * + * These events can be logged by calling `.emit()` on them if a single event, or calling + * [`FtMint::emit_many`], [`FtTransfer::emit_many`], + * or [`FtBurn::emit_many`] respectively. + */ + +import { NearEvent } from "../event"; +import { Option } from "../non_fungible_token/utils"; +import { AccountId, Balance } from "near-sdk-js"; + +export type Nep141EventKind = FtMint[] | FtTransfer[] | FtBurn[]; + +export class Nep141Event extends NearEvent { + version: string; + event_kind: Nep141EventKind; + + constructor(version: string, event_kind: Nep141EventKind) { + super(); + this.version = version; + this.event_kind = event_kind; + } + } + +/** Data to log for an FT mint event. To log this event, call [`.emit()`](FtMint::emit). */ +export class FtMint { + owner_id: AccountId; + amount: number; + memo: Option; + + constructor(owner_id: AccountId, amount: number, memo: Option) { + this.owner_id = owner_id; + this.amount = amount; + this.memo = memo; + } + + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]) + } + + /** Emits an FT mint event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtMint`] represents the data of each mint. + */ + emit_many(data: FtMint[]) { + new_141_v1(data).emit() + } +} + +/** Data to log for an FT transfer event. To log this event, + * call [`.emit()`](FtTransfer::emit). + */ +export class FtTransfer { + old_owner_id: AccountId; + new_owner_id: AccountId; + amount: string; + memo: Option; + + constructor(old_owner_id: AccountId, new_owner_id: AccountId, amount: bigint, memo: Option) { + this.old_owner_id = old_owner_id; + this.new_owner_id = new_owner_id; + this.amount = amount.toString(); + this.memo = memo; + } + + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]) + } + + /** Emits an FT transfer event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtTransfer`] represents the data of each transfer. + */ + emit_many(data: FtTransfer[]) { + new_141_v1(data).emit() + } +} + +/** Data to log for an FT burn event. To log this event, call [`.emit()`](FtBurn::emit). */ +export class FtBurn { + owner_id: AccountId; + amount: string; + memo: Option; + + constructor(owner_id: AccountId, amount: Balance, memo: Option) { + this.owner_id = owner_id; + this.amount = amount.toString(); + this.memo = memo; + } + + /** Logs the event to the host. This is required to ensure that the event is triggered + * and to consume the event. + */ + emit() { + this.emit_many([this]) + } + + /** Emits an FT burn event, through [`env::log_str`](near_sdk::env::log_str), + * where each [`FtBurn`] represents the data of each burn. + */ + emit_many(data: FtBurn[]) { + new_141_v1(data).emit() + } +} + +function new_141(version: string, event_kind: Nep141EventKind): NearEvent { + return new Nep141Event(version, event_kind); +} + +function new_141_v1(event_kind: Nep141EventKind) : NearEvent { + return new_141("1.0.0", event_kind) +} + diff --git a/packages/near-contract-standards/src/fungible_token/index.ts b/packages/near-contract-standards/src/fungible_token/index.ts new file mode 100644 index 000000000..037205190 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/index.ts @@ -0,0 +1,6 @@ +export * from './core_impl'; +export * from './core'; +export * from './events'; +export * from './metadata'; +export * from './receiver'; +export * from './resolver'; \ No newline at end of file diff --git a/packages/near-contract-standards/src/fungible_token/metadata.ts b/packages/near-contract-standards/src/fungible_token/metadata.ts new file mode 100644 index 000000000..81380dc86 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/metadata.ts @@ -0,0 +1,49 @@ +import { + assert, +} from "near-sdk-js"; + +import { Option } from "../non_fungible_token/utils"; + +const FT_METADATA_SPEC: string = "ft-1.0.0"; + +export class FungibleTokenMetadata { + spec: string; + name: string; + symbol: string; + icon: Option; + reference: Option; + reference_hash: Option; + decimals: number; + + constructor( + spec: string, + name: string, + symbol: string, + icon: Option, + referance: Option, + referance_hash: Option, + decimals: number, + ) { + this.spec = spec; + this.name = name; + this.symbol = symbol; + this.icon = icon; + this.reference = referance; + this.reference_hash = referance_hash; + this.decimals = decimals; + } + + assert_valid() { + assert(this.spec == FT_METADATA_SPEC, "Invalid FT_METADATA_SPEC"); + const isReferenceProvided: boolean = this.reference ? true : false; + const isReferenceHashProvided: boolean = this.reference_hash ? true : false; + assert(isReferenceHashProvided === isReferenceProvided, "reference and reference_hash must be either both provided or not"); + if (this.reference_hash) { + assert(this.reference_hash.length === 32, "reference_hash must be 32 bytes"); + } + } +} + +export interface FungibleTokenMetadataProvider { + ft_metadata() : FungibleTokenMetadata; +} diff --git a/packages/near-contract-standards/src/fungible_token/receiver.ts b/packages/near-contract-standards/src/fungible_token/receiver.ts new file mode 100644 index 000000000..61f022e20 --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/receiver.ts @@ -0,0 +1,34 @@ +import { AccountId, PromiseOrValue } from "near-sdk-js"; + +export interface FungibleTokenReceiver { + /** + * Called by fungible token contract after `ft_transfer_call` was initiated by + * `sender_id` of the given `amount` with the transfer message given in `msg` field. + * The `amount` of tokens were already transferred to this contract account and ready to be used. + * + * The method must return the amount of tokens that are *not* used/accepted by this contract from the transferred + * amount. Examples: + * - The transferred amount was `500`, the contract completely takes it and must return `0`. + * - The transferred amount was `500`, but this transfer call only needs `450` for the action passed in the `msg` + * field, then the method must return `50`. + * - The transferred amount was `500`, but the action in `msg` field has expired and the transfer must be + * cancelled. The method must return `500` or panic. + * + * Arguments: + * @param sender_id - the account ID that initiated the transfer. + * @param amount - the amount of tokens that were transferred to this account in a decimal string representation. + * @param msg - a string message that was passed with this transfer call. + * + * @returns the amount of unused tokens that should be returned to sender, in a decimal string representation. + */ + ft_on_transfer({ + sender_id, + amount, + msg + }: { + sender_id: AccountId, + amount: number, + msg: String + } + ): PromiseOrValue; +} diff --git a/packages/near-contract-standards/src/fungible_token/resolver.ts b/packages/near-contract-standards/src/fungible_token/resolver.ts new file mode 100644 index 000000000..6f2d0a12e --- /dev/null +++ b/packages/near-contract-standards/src/fungible_token/resolver.ts @@ -0,0 +1,13 @@ +import { AccountId, Balance } from "near-sdk-js"; + +export interface FungibleTokenResolver { + ft_resolve_transfer({ + sender_id, + receiver_id, + amount, + }: { + sender_id: AccountId, + receiver_id: AccountId, + amount: Balance, + }): Balance; +} diff --git a/packages/near-contract-standards/src/index.ts b/packages/near-contract-standards/src/index.ts index 20839e61e..508384aff 100644 --- a/packages/near-contract-standards/src/index.ts +++ b/packages/near-contract-standards/src/index.ts @@ -1,2 +1,6 @@ -/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken/README.html). */ +/** Non-fungible tokens as described in [by the spec](https://nomicon.io/Standards/NonFungibleToken). */ export * from "./non_fungible_token"; +/** Fungible tokens as described in [by the spec](https://nomicon.io/Standards/FungibleToken). */ +export * from "./fungible_token"; + +export * from "./storage_management"; \ No newline at end of file diff --git a/packages/near-contract-standards/src/storage_management/index.ts b/packages/near-contract-standards/src/storage_management/index.ts new file mode 100644 index 000000000..86094508e --- /dev/null +++ b/packages/near-contract-standards/src/storage_management/index.ts @@ -0,0 +1,72 @@ +import { AccountId, Balance } from "near-sdk-js" +import { Option } from "../non_fungible_token/utils"; + +export class StorageBalance { + total: Balance; + available: Balance; + + constructor(total: Balance, available: Balance) { + this.total = total; + this.available = available; + } +} + +export class StorageBalanceBounds { + constructor(min: Balance, max: Option) { + this.min = min; + this.max = max; + } + + min: Balance; + max: Option; +} + +export interface StorageManagement { + /** + * @param registration_only if `true` MUST refund above the minimum balance if the account didn't exist and + * refund full deposit if the account exists. + */ + storage_deposit( + { + account_id, + registration_only + }: { + account_id: Option, + registration_only: Option, + } + ): StorageBalance; + + /** Withdraw specified amount of available Ⓝ for predecessor account. + * + * This method is safe to call. It MUST NOT remove data. + * + * @param amount is sent as a string representing an unsigned 128-bit integer. If + * omitted, contract MUST refund full `available` balance. If `amount` exceeds + * predecessor account's available balance, contract MUST panic. + * + * If predecessor account not registered, contract MUST panic. + * + * MUST require exactly 1 yoctoNEAR attached balance to prevent restricted + * function-call access-key call (UX wallet security) + * + * @returns the StorageBalance structure showing updated balances. + */ + storage_withdraw({ amount }: { amount?: bigint }): StorageBalance; + + /** Unregisters the predecessor account and returns the storage NEAR deposit back. + * + * If the predecessor account is not registered, the function MUST return `false` without panic. + * + * @param force If `force=true` the function SHOULD ignore account balances (burn them) and close the account. + * Otherwise, MUST panic if caller has a positive registered balance (eg token holdings) or + * the contract doesn't support force unregistration. + * MUST require exactly 1 yoctoNEAR attached balance to prevent restricted function-call access-key call + * (UX wallet security) + * @returns `true` if the account was unregistered, `false` if account was not registered before. + */ + storage_unregister({ force }: { force: Option }): boolean; + + storage_balance_bounds(): StorageBalanceBounds; + + storage_balance_of({ account_id }: { account_id: AccountId }): Option; +} diff --git a/packages/near-contract-standards/tsconfig.json b/packages/near-contract-standards/tsconfig.json index 247f4d538..252a8d96f 100644 --- a/packages/near-contract-standards/tsconfig.json +++ b/packages/near-contract-standards/tsconfig.json @@ -23,8 +23,6 @@ }, "files": [ "src/index.ts", - "src/non_fungible_token/approval/approval_receiver.ts", - "src/non_fungible_token/core/receiver.ts" ], "exclude": ["node_modules"] } diff --git a/packages/near-sdk-js/lib/promise.d.ts b/packages/near-sdk-js/lib/promise.d.ts index 8e4b62bca..691200a1f 100644 --- a/packages/near-sdk-js/lib/promise.d.ts +++ b/packages/near-sdk-js/lib/promise.d.ts @@ -170,7 +170,7 @@ export declare class AddAccessKey extends PromiseAction { /** * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */ @@ -319,7 +319,7 @@ export declare class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. */ addAccessKey(publicKey: PublicKey, allowance: Balance, receiverId: AccountId, functionNames: string): NearPromise; @@ -329,7 +329,7 @@ export declare class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */ diff --git a/packages/near-sdk-js/lib/promise.js b/packages/near-sdk-js/lib/promise.js index 1c7872bdc..f40080105 100644 --- a/packages/near-sdk-js/lib/promise.js +++ b/packages/near-sdk-js/lib/promise.js @@ -192,7 +192,7 @@ export class AddAccessKey extends PromiseAction { /** * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */ @@ -406,7 +406,7 @@ export class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. */ addAccessKey(publicKey, allowance, receiverId, functionNames) { @@ -418,7 +418,7 @@ export class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */ diff --git a/packages/near-sdk-js/src/promise.ts b/packages/near-sdk-js/src/promise.ts index 3db1f260c..79359887a 100644 --- a/packages/near-sdk-js/src/promise.ts +++ b/packages/near-sdk-js/src/promise.ts @@ -251,7 +251,7 @@ export class AddAccessKey extends PromiseAction { /** * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */ @@ -529,7 +529,7 @@ export class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. */ addAccessKey( @@ -553,7 +553,7 @@ export class NearPromise { * * @param publicKey - The public key to add as a access key. * @param allowance - The allowance for the key in yoctoNEAR. - * @param receiverId - The account ID of the reciever. + * @param receiverId - The account ID of the receiver. * @param functionNames - The names of funcitons to authorize. * @param nonce - The nonce to use. */