Skip to content

Commit

Permalink
docs: Fungible Token for AI conversation (#21)
Browse files Browse the repository at this point in the history
* fungible token ai conversation docs

* view method support

* 128_000_000 tokens to start an AI conversation

* refund

* include mermaid flow diagram
  • Loading branch information
petersalomonsen authored Oct 28, 2024
1 parent cfa390c commit 3defbb6
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 56 deletions.
134 changes: 121 additions & 13 deletions examples/fungibletoken/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,127 @@ Below is an example from the tests, that transfers a fixed amount of `2000` toke

One of the intended use cases of this is to be able to reserve the max needed tokens for an AI request, and then return back to the caller the unspent tokens.

```javascript
export function transfer_2_000_to_alice() {
const amount = 2_000n;
const transfer_id = env.signer_account_id() + '_' + new Date().getTime();
env.set_data(transfer_id, JSON.stringify({receiver_id: env.signer_account_id(), refund_amount: (amount / 2n).toString()}));
env.ft_transfer('alice.test.near', amount.toString());
env.value_return(transfer_id);
}
See [aiconversation.js](./e2e/aiconversation.js) for an example of the added JavaScript code.

## High level overview of the flow of an AI conversation

```mermaid
sequenceDiagram
participant User
participant Contract
participant AIService
User->>Contract: call_js_func(start_ai_conversation)
Contract->>Contract: Generate conversation_id
Contract->>Contract: ft_transfer_internal(user, AIService, amount)
Contract->>User: Return conversation_id
User->>AIService: Initiate AI request with conversation_id
AIService->>Contract: Check deposit for conversation_id
Contract->>AIService: Confirm deposited tokens
AIService->>User: Provide AI-generated content
AIService->>AIService: Track spent AI tokens
AIService->>User: Sign message with unspent tokens
User->>Contract: call_js_func(refund_unspent, {signature, refund_message})
Contract->>Contract: Verify signature
Contract->>Contract: Process refund
Contract->>User: Return unspent tokens
```

# Deploying and creating your token

Build the contract by running the script [build.sh](./build.sh).

Then you can deploy the contract, using [near-cli-rs](https://github.com/near/near-cli-rs), with the command below (replace with your own account and preferred signing method).

```bash
near contract deploy aitoken.testnet use-file out/fungible_token.wasm without-init-call network-config testnet sign-with-keychain send
```

To mint the Fungible Token you can call the `new` function on the contract. Here is an example:

```bash
near contract call-function as-transaction aitoken.testnet new json-args '{"owner_id": "aitoken.testnet", "total_supply": "999999999999", "metadata": { "spec": "ft-1.0.0","name": "W-awesome AI token","symbol": "WASMAI","decimals": 6}}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as aitoken.testnet network-config testnet sign-with-keychain send
```

# Posting Javascript to the contract

You can post your custom Javascript to the contract by calling the `post_javascript` function. You don't need a full access key to do this, a function access key owned by the contract account is sufficient. Here is an example posting the JavaScript file [aiconversation.js](./e2e/aiconversation.js).

```bash
near contract call-function as-transaction aitoken.testnet post_javascript json-args "$(jq -Rs '{javascript: .}' < e2e/aiconversation.js)" prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as aitoken.testnet network-config testnet sign-with-keychain send
```

# Interacting with the contract

## Preparing, transferring Fungible Tokens to the user account

A user wants to start an AI conversation. Before doing so, the user needs to be obtain some amount of the fungible tokens we just minted. Before an account can receive fungible tokens, it has to be registered with the contract. The user account can register itself.

```bash
near contract call-function as-transaction aitoken.testnet storage_deposit json-args '{"account_id": "aiuser.testnet"}' prepaid-gas '100.0 Tgas' attached-deposit '0.01 near' sign-as aiuser.testnet network-config testnet sign-with-keychain send
```

Now we can transfer 1,000,000,000 tokens to a `aiuser.testnet` using the following command:

export function refund() {
const { transfer_id } = JSON.parse(env.input());
const {refund_amount, receiver_id} = JSON.parse(env.get_data(transfer_id));
env.clear_data(transfer_id);
env.ft_transfer(receiver_id, refund_amount);
```bash
near contract call-function as-transaction aitoken.testnet ft_transfer json-args '{"receiver_id": "aiuser.testnet", "amount": "1000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '1 yoctonear' sign-as aitoken.testnet network-config testnet sign-with-keychain send
```

And we can also see that the token balance for `aiuser.testnet` is `1000000000` by running the following command:

```bash
near contract call-function as-read-only aitoken.testnet ft_balance_of json-args '{"account_id": "aiuser.testnet"}' network-config testnet now
```

## Starting the AI conversation

We can call the Javascript function `start_ai_conversation`:

```bash
near contract call-function as-transaction aitoken.testnet call_js_func json-args '{"function_name": "start_ai_conversation"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as aiuser.testnet network-config testnet sign-with-keychain send
```

The result of this function call should be a conversation id, that consists of the account name and a timestamp. For example it can be `aiuser.testnet_1730058753308`.

## The AI service

Before serving AI generated content, the AI service will contact the contract to see if the conversation has tokens deposited. It will make a view call to the function `view_ai_conversation` to check the registered data for the given `conversation_id`.

We can also make this view call to see the registered data for the conversation.

```bash
near contract call-function as-read-only aitoken.testnet view_js_func json-args '{"function_name": "view_ai_conversation", "conversation_id": "aiuser.testnet_1730058753308"}' network-config testnet now
```

We should see a result like this:

```
{
"amount": "128000000",
"receiver_id": "aiuser.testnet"
}
```

The AI service is now good to go, and will track usage for this conversation.
To the AI service UI, you provide the conversation ID. The AI service will look up the initial conversation balance from the contract and track usage as the conversation goes on.

![Screenshot of conversation UI](uiscreenshot.png)

When the user wants to end the conversation, the `Stop conversation and refund tokens` button can be clicked, and a signed refund message will be displayed. No more messages can be added to the conversation.


```json
{"refund_message": "{\"conversation_id\":\"aiuser.testnet_1730058753308\",\"receiver_id\":\"aiuser.testnet\",\"refund_amount\":\"127999584\"}","signature": [ 45, 73, 50, 99, 128, 29, 74, 56, 160, 85, 146, 64, 96, 15, 236, 191, 82, 234, 108, 224, 55, 161, 123, 122, 122, 102, 236, 33, 173, 193, 93, 177, 105, 95, 249, 58, 65, 107, 136, 169, 36, 254, 86, 184, 27, 224, 226, 164, 66, 40, 94, 123, 111, 196, 16, 126, 92, 190, 37, 210, 158, 132, 13, 10]}
```

We can post this refund message back to the smart contract.

```bash
near contract call-function as-transaction aitoken.testnet call_js_func json-args '{"function_name": "refund_unspent", "refund_message": "{\"conversation_id\":\"aiuser.testnet_1730058753308\",\"receiver_id\":\"aiuser.testnet\",\"refund_amount\":\"127999584\"}","signature": [ 45, 73, 50, 99, 128, 29, 74, 56, 160, 85, 146, 64, 96, 15, 236, 191, 82, 234, 108, 224, 55, 161, 123, 122, 122, 102, 236, 33, 173, 193, 93, 177, 105, 95, 249, 58, 65, 107, 136, 169, 36, 254, 86, 184, 27, 224, 226, 164, 66, 40, 94, 123, 111, 196, 16, 126, 92, 190, 37, 210, 158, 132, 13, 10]}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as aiuser.testnet network-config testnet sign-with-keychain send
```

And we can then see that `aiuser.testnet` has received 127999584 tokens as a refund for the unspent tokens in the conversation. If we call the view method above for viewing the AI conversation data, we can see that the result is `undefined`, and if we call the method for seeing the token balance of `aiuser.testnet` we can see that it has an increased balance.
28 changes: 28 additions & 0 deletions examples/fungibletoken/e2e/aiconversation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function start_ai_conversation() {
const amount = 128_000_000n;
let conversation_id = env.signer_account_id() + "_" + (new Date().getTime());
env.set_data(conversation_id, JSON.stringify({ receiver_id: env.signer_account_id(), amount: amount.toString() }));
env.ft_transfer_internal(env.signer_account_id(), 'aitoken.testnet', amount.toString());
env.value_return(conversation_id);
}

export function view_ai_conversation() {
const { conversation_id } = JSON.parse(env.input());
env.value_return(env.get_data(conversation_id));
}

export function refund_unspent() {
const { refund_message, signature } = JSON.parse(env.input());
const public_key = new Uint8Array([74, 228, 41, 15, 116, 28, 61, 128, 166, 59, 142, 157, 249, 47, 117, 247, 100, 245, 49, 238, 11, 198, 191, 189, 249, 115, 108, 241, 114, 247, 26, 166]);

const signature_is_valid = env.ed25519_verify(new Uint8Array(signature), new Uint8Array(env.sha256_utf8(refund_message)), public_key);
if (signature_is_valid) {
const { receiver_id, refund_amount, conversation_id } = JSON.parse(refund_message);
const conversation_data = JSON.parse(env.get_data(conversation_id));
if (BigInt(conversation_data.amount) > BigInt(refund_amount)) {
env.clear_data(conversation_id);
env.ft_transfer_internal('aitoken.testnet', receiver_id, refund_amount);
print(`refunded ${refund_amount} to ${receiver_id}`);
}
}
}
23 changes: 17 additions & 6 deletions examples/fungibletoken/e2e/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,14 @@ describe('Fungible token contract', { only: true }, () => {

afterEach(async () => {
const aliceBalance = await contract.view('ft_balance_of', { account_id: 'alice.test.near' });
await alice.call(contract.accountId, 'ft_transfer', {
receiver_id: 'bob.test.near',
amount: aliceBalance.toString(),
}, {
attachedDeposit: 1n.toString()
});
if (BigInt(aliceBalance) > 0n) {
await alice.call(contract.accountId, 'ft_transfer', {
receiver_id: 'bob.test.near',
amount: aliceBalance.toString(),
}, {
attachedDeposit: 1n.toString()
});
}
});

test('should run custom javascript transfer functions in contract', async () => {
Expand Down Expand Up @@ -181,6 +183,11 @@ describe('Fungible token contract', { only: true }, () => {
env.value_return(conversation_id);
}
export function view_ai_conversation() {
const { conversation_id } = JSON.parse(env.input());
env.value_return(env.get_data(conversation_id));
}
export function refund_unspent() {
const { refund_message, signature } = JSON.parse(env.input());
const public_key = new Uint8Array([${Array.from((await bob.getKey()).getPublicKey().data).toString()}]);
Expand Down Expand Up @@ -218,6 +225,10 @@ describe('Fungible token contract', { only: true }, () => {

expect(conversation_id.split("_")[0]).to.equal("alice.test.near");
expect(await contract.view('ft_balance_of', { account_id: 'alice.test.near' })).to.equal(0n.toString());
const conversation_data = await contract.view('view_js_func', { function_name: "view_ai_conversation", conversation_id });

expect(conversation_data.receiver_id).to.equal('alice.test.near');
expect(conversation_data.amount).to.equal(2000n.toString());

const refund_message = JSON.stringify({ receiver_id: 'alice.test.near', refund_amount: 1000n.toString() });
const refund_message_hashed = createHash('sha256').update(Buffer.from(refund_message, 'utf8')).digest();
Expand Down
19 changes: 12 additions & 7 deletions examples/fungibletoken/meta-dce.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
[
{
"name": "call_js_func",
"export": "call_js_func",
"root": true
"name": "call_js_func",
"export": "call_js_func",
"root": true
},
{
"name": "post_javascript",
"export": "post_javascript",
"root": true
"name": "view_js_func",
"export": "view_js_func",
"root": true
},
{
"name": "post_javascript",
"export": "post_javascript",
"root": true
},
{
"name": "ft_transfer",
Expand Down Expand Up @@ -69,4 +74,4 @@
"export": "new",
"root": true
}
]
]
79 changes: 49 additions & 30 deletions examples/fungibletoken/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ pub struct Contract {
data_map: near_sdk::collections::LookupMap<String, String>,
}

static mut CONTRACT_REF: *mut Contract = 0 as *mut Contract;
const DATA_IMAGE_SVG_NEAR_ICON: &str = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 288 288'%3E%3Cg id='l' data-name='l'%3E%3Cpath d='M187.58,79.81l-30.1,44.69a3.2,3.2,0,0,0,4.75,4.2L191.86,103a1.2,1.2,0,0,1,2,.91v80.46a1.2,1.2,0,0,1-2.12.77L102.18,77.93A15.35,15.35,0,0,0,90.47,72.5H87.34A15.34,15.34,0,0,0,72,87.84V201.16A15.34,15.34,0,0,0,87.34,216.5h0a15.35,15.35,0,0,0,13.08-7.31l30.1-44.69a3.2,3.2,0,0,0-4.75-4.2L96.14,186a1.2,1.2,0,0,1-2-.91V104.61a1.2,1.2,0,0,1,2.12-.77l89.55,107.23a15.35,15.35,0,0,0,11.71,5.43h3.13A15.34,15.34,0,0,0,216,201.16V87.84A15.34,15.34,0,0,0,200.66,72.5h0A15.35,15.35,0,0,0,187.58,79.81Z'/%3E%3C/g%3E%3C/svg%3E";
static mut CONTRACT_REF_MUT: *mut Contract = 0 as *mut Contract;
static mut CONTRACT_REF: *const Contract = 0 as *const Contract;

#[near_bindgen]
impl Contract {
Expand Down Expand Up @@ -97,13 +98,14 @@ impl Contract {
return load_js_bytecode(bytecode.as_ptr(), bytecode.len());
}

unsafe fn add_js_functions(&mut self) {
unsafe fn add_mut_js_functions(&mut self) {
CONTRACT_REF_MUT = self as *mut Contract;
add_function_to_js(
"clear_data",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let key = arg_to_str(ctx, 0, argv);
let value = arg_to_str(ctx, 1, argv);
(*CONTRACT_REF).data_map.insert(&key, &value);
(*CONTRACT_REF_MUT).data_map.insert(&key, &value);
0
},
2,
Expand All @@ -114,12 +116,44 @@ impl Contract {
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let key = arg_to_str(ctx, 0, argv);
let value = arg_to_str(ctx, 1, argv);
(*CONTRACT_REF).data_map.insert(&key, &value);
(*CONTRACT_REF_MUT).data_map.insert(&key, &value);
0
},
2,
);

add_function_to_js(
"ft_transfer",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let receiver_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 1, argv).parse::<u128>().unwrap());
(*CONTRACT_REF_MUT).ft_transfer(receiver_id, amount, None);
return 0;
},
2,
);

add_function_to_js(
"ft_transfer_internal",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let sender_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let receiver_id = arg_to_str(ctx, 1, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 2, argv).parse::<u128>().unwrap());
(*CONTRACT_REF_MUT).token.internal_transfer(
&sender_id,
&receiver_id,
amount.0,
None,
);
return 0;
},
2,
);
}

unsafe fn add_js_functions(&self) {
CONTRACT_REF = self as *const Contract;

add_function_to_js(
"get_data",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
Expand All @@ -133,7 +167,6 @@ impl Contract {
1,
);

CONTRACT_REF = self as *mut Contract;
add_function_to_js(
"ft_balance_of",
move |ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
Expand All @@ -143,37 +176,23 @@ impl Contract {
},
1,
);

add_function_to_js(
"ft_transfer",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let receiver_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 1, argv).parse::<u128>().unwrap());
(*CONTRACT_REF).ft_transfer(receiver_id, amount, None);
return 0;
},
2,
);

add_function_to_js(
"ft_transfer_internal",
|ctx: i32, _this_val: i64, _argc: i32, argv: i32| -> i64 {
let sender_id = arg_to_str(ctx, 0, argv).parse().unwrap();
let receiver_id = arg_to_str(ctx, 1, argv).parse().unwrap();
let amount: U128 = U128(arg_to_str(ctx, 2, argv).parse::<u128>().unwrap());
(*CONTRACT_REF)
.token
.internal_transfer(&sender_id, &receiver_id, amount.0, None);
return 0;
},
2,
);
}

#[payable]
pub fn call_js_func(&mut self, function_name: String) {
let jsmod = self.load_js_bytecode();

unsafe {
self.add_js_functions();
self.add_mut_js_functions();
let function_name_cstr = CString::new(function_name).unwrap();
js_call_function(jsmod, function_name_cstr.as_ptr() as i32);
}
}

pub fn view_js_func(&self, function_name: String) {
let jsmod = self.load_js_bytecode();

unsafe {
self.add_js_functions();
let function_name_cstr = CString::new(function_name).unwrap();
Expand Down
Binary file added examples/fungibletoken/uiscreenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3defbb6

Please sign in to comment.