Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ed25519 verify #19

Merged
merged 7 commits into from
Oct 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
582 changes: 431 additions & 151 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ strip = false

[dependencies]
near-sdk = "5.5.0"
ed25519-dalek = "1.0.1"
ed25519-dalek = "2.1.1"
sha2 = "0.10.6"
hex = "0.4.3"
quickjs-rust-near-testenv = { path = "testenv" }
Expand All @@ -40,4 +40,4 @@ members = [
"examples/purejs",
"testenv",
"."
]
]
49 changes: 47 additions & 2 deletions e2e/e2e.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Worker } from 'near-workspaces';
import { KeyPair, Worker } from 'near-workspaces';
import { before, after, test, describe } from 'node:test';
import { expect } from 'chai';

describe('run simple js', () => {
describe('run simple js', { only: true }, () => {
/**
* @type {Worker}
*/
Expand Down Expand Up @@ -94,4 +94,49 @@ env.value_return(JSON.stringify(result)); `
expect(result).to.equal('valid');
}, 40000);

test('should verify signed message using ed25519_verify', { only: true }, async () => {
/**
* @type {KeyPair}
*/
const keyPair = contractAccountKeyPair;
const messageToBeSigned = 'the expected message to be signed';

const messageBytes = new TextEncoder().encode(messageToBeSigned);
const signature = Array.from((await keyPair.sign(messageBytes)).signature);
const message = Array.from(messageBytes);
const public_key = Array.from(keyPair.getPublicKey().data);

await contract.call(
contract.accountId,
'submit_script',
{
script: `
const args = JSON.parse(env.input());
const result = env.ed25519_verify(new Uint8Array(args.signature), new Uint8Array(args.message), new Uint8Array(args.public_key));
env.value_return(JSON.stringify(result ? 'valid' : 'invalid'));
`
}
);

let result = await contract.view(
'run_script_for_account_no_return',
{
account_id: contract.accountId,
message,
signature,
public_key
}
);
expect(result).to.equal('valid');
result = await contract.view(
'run_script_for_account_no_return',
{
account_id: contract.accountId,
message: [3, 2, 5, 1],
signature,
public_key
}
);
expect(result).to.equal('invalid');
}, 40000);
});
149 changes: 134 additions & 15 deletions examples/fungibletoken/e2e/e2e.test.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { connect, keyStores } from 'near-api-js';
import { Worker } from 'near-workspaces';
import { before, after, test, describe } from 'node:test';
import { before, after, test, describe, afterEach } from 'node:test';
import { expect } from 'chai';
import { createHash } from 'crypto';

const connectionConfig = {
networkId: "sandbox",
keyStore: new keyStores.InMemoryKeyStore(),
nodeUrl: "https://rpc.testnet.near.org"
};

describe('Fungible token contract', () => {
describe('Fungible token contract', { only: true }, () => {
/**
* @type {Worker}
*/
Expand All @@ -24,7 +25,13 @@ describe('Fungible token contract', () => {
* @type {import('near-workspaces').NearAccount}
*/
let contract;
/**
* @type {import('near-workspaces').NearAccount}
*/
let bob;
/**
* @type {import('near-workspaces').NearAccount}
*/
let alice;

/**
Expand All @@ -46,12 +53,28 @@ describe('Fungible token contract', () => {
await contract.call(contract.accountId, 'new_default_meta', { owner_id: 'bob.test.near', total_supply: 1_000_000n.toString() });
contractAccountKeyPair = await contract.getKey();
connectionConfig.keyStore.setKey("sandbox", contract.accountId, contractAccountKeyPair);
await alice.call(contract.accountId, 'storage_deposit', {
account_id: 'alice.test.near',
registration_only: true,
}, {
attachedDeposit: 1_0000_0000000000_0000000000n.toString()
});
});
after(async () => {
await worker.tearDown();
});

test('should run custom javascript in contract', async () => {
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()
});
});

test('should run custom javascript transfer functions in contract', async () => {
const nearConnection = await connect(connectionConfig);
const accountId = contract.accountId;

Expand Down Expand Up @@ -79,18 +102,6 @@ describe('Fungible token contract', () => {
`
}
});
await bob.call(accountId, 'storage_deposit', {
account_id: 'bob.test.near',
registration_only: true,
}, {
attachedDeposit: 1_0000_0000000000_0000000000n.toString()
});
await alice.call(accountId, 'storage_deposit', {
account_id: 'alice.test.near',
registration_only: true,
}, {
attachedDeposit: 1_0000_0000000000_0000000000n.toString()
});

expect(await contract.view('ft_balance_of', { account_id: 'bob.test.near' })).to.equal(1_000_000n.toString());

Expand All @@ -113,4 +124,112 @@ describe('Fungible token contract', () => {
expect(await contract.view('ft_balance_of', { account_id: 'bob.test.near' })).to.equal(999_000n.toString());
expect(await contract.view('ft_balance_of', { account_id: 'alice.test.near' })).to.equal(1_000n.toString());
});

test('should not double gas usage when calling transfer via JS', { only: false }, async () => {
const nearConnection = await connect(connectionConfig);
const accountId = contract.accountId;

const account = await nearConnection.account(accountId);
await account.functionCall({
contractId: accountId,
methodName: 'post_javascript',
gas: '300000000000000',
args: {
javascript: `
export function ft_transfer_js() {
const { amount, receiver_id } = JSON.parse(env.input());
env.ft_transfer(receiver_id, amount);
}
`
}
});

let result = await bob.callRaw(accountId, 'call_js_func',
{
function_name: "ft_transfer_js",
receiver_id: "alice.test.near",
amount: "2000"
}, {
attachedDeposit: '1'
});
expect(await contract.view('ft_balance_of', { account_id: 'alice.test.near' })).to.equal(2_000n.toString());
const totalGasBurntJS = result.result.receipts_outcome.reduce((prev, receipt_outcome) => prev + receipt_outcome.outcome.gas_burnt, 0);
result = await bob.callRaw(accountId, 'ft_transfer',
{
receiver_id: "alice.test.near",
amount: "2000"
}, {
attachedDeposit: '1'
});
expect(await contract.view('ft_balance_of', { account_id: 'alice.test.near' })).to.equal(4_000n.toString());
const totalGasBurnt = result.result.receipts_outcome.reduce((prev, receipt_outcome) => prev + receipt_outcome.outcome.gas_burnt, 0);

expect(totalGasBurntJS / totalGasBurnt).to.be.lessThan(2.0);
});

test('should run custom javascript transfer functions in contract with function access keys, and without attaching deposits', { only: true }, async () => {
const nearConnection = await connect(connectionConfig);
const accountId = contract.accountId;

const account = await nearConnection.account(accountId);
const javascript = `
export function start_ai_conversation() {
const amount = 2_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(), 'bob.test.near', amount.toString());
env.value_return(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()}]);

const signature_is_valid = env.ed25519_verify(new Uint8Array(signature), new Uint8Array(env.sha256_utf8(refund_message)) , public_key);
if (signature_is_valid) {
print("REFUNDING");
const { receiver_id, refund_amount } = JSON.parse(refund_message);
env.ft_transfer_internal('bob.test.near', receiver_id, refund_amount);
} else {
print("INVALID SIGNATURE");
}
}
`;

await account.functionCall({
contractId: accountId,
methodName: 'post_javascript',
gas: '300000000000000',
args: {
javascript
}
});

await bob.call(accountId, 'ft_transfer', {
receiver_id: 'alice.test.near',
amount: 2000n.toString(),
}, {
attachedDeposit: 1n.toString()
});

const conversation_id = await alice.call(accountId, 'call_js_func', {
function_name: "start_ai_conversation"
});

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 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();
const signature = (await bob.getKey()).sign(Uint8Array.from(refund_message_hashed));

await bob.call(accountId, 'call_js_func',
{
function_name: "refund_unspent",
signature: Array.from(signature.signature),
refund_message
});

expect(await contract.view('ft_balance_of', { account_id: 'alice.test.near' })).to.equal(1_000n.toString());
});
});
1 change: 1 addition & 0 deletions localjstestenv/wasm-near-environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,4 @@ export function storage_has_key(key_len, key_ptr) {
}
export function validator_stake() { }
export function validator_total_stake() { }
export function ed25519_verify() { }
1 change: 1 addition & 0 deletions quickjslib/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.wasm
*.a
*.o
*.o*
Loading
Loading