diff --git a/.gitignore b/.gitignore index 8e20e1f..077aa39 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ emsdk /blob-report/ /playwright/.cache/ bin - +*.mp4 diff --git a/examples/aiproxy/.test.env b/examples/aiproxy/.test.env index 174d6d3..2894be3 100644 --- a/examples/aiproxy/.test.env +++ b/examples/aiproxy/.test.env @@ -1,5 +1,5 @@ SPIN_VARIABLE_OPENAI_API_KEY=abcd -SPIN_VARIABLE_REFUND_SIGNING_KEY=defgh +SPIN_VARIABLE_REFUND_SIGNING_KEY=48QM3KLHFY22hDNnDx6zvgakY1dy66Jsv4dtTT6mt131DtjvPrQn7zyr3CVb1ZKPuVLbmvjQSK9o5vuEvMyiLR5Y SPIN_VARIABLE_FT_CONTRACT_ID=aitoken.test.near SPIN_VARIABLE_OPENAI_COMPLETIONS_ENDPOINT=http://127.0.0.1:3001/v1/chat/completions SPIN_VARIABLE_RPC_URL=http://localhost:14500 \ No newline at end of file diff --git a/examples/aiproxy/playwright-tests/aiproxy.spec.js b/examples/aiproxy/playwright-tests/aiproxy.spec.js index 5b20f40..81d9964 100644 --- a/examples/aiproxy/playwright-tests/aiproxy.spec.js +++ b/examples/aiproxy/playwright-tests/aiproxy.spec.js @@ -2,13 +2,28 @@ import { test, expect } from '@playwright/test'; test('ask question', async ({ page }) => { + const { functionAccessKeyPair, publicKey, accountId, contractId } = await fetch('http://localhost:14501').then(r => r.json()); + await page.goto('/'); + await page.evaluate(({ accountId, publicKey, functionAccessKeyPair, contractId }) => { + localStorage.setItem("aiproxy_wallet_auth_key", JSON.stringify({ accountId, allKeys: [publicKey] })); + localStorage.setItem(`near-api-js:keystore:${accountId}:sandbox`, functionAccessKeyPair); + localStorage.setItem(`contractId`, contractId); + }, { accountId, publicKey, functionAccessKeyPair, contractId }); + + await page.reload(); - const {real_conversation_id} = await fetch('http://localhost:14501').then(r => r.json()); - await page.getByPlaceholder('conversation id').fill(real_conversation_id); + await page.getByRole('button', { name: 'Start conversation' }).click(); const questionArea = await page.getByPlaceholder('Type your question here...'); + await expect(questionArea).toBeEnabled(); questionArea.fill("Hello!"); - await page.getByRole('button', { name: 'Ask OpenAI' }).click(); - await expect(await page.getByText("I am just a mockserver")).toBeVisible(); + await page.waitForTimeout(500); + await page.getByRole('button', { name: 'Ask AI' }).click(); + await expect(await page.getByText("Hello! How can I assist you today?")).toBeVisible(); + + await page.waitForTimeout(500); + await page.locator("#refundButton").click(); + + await expect(await page.locator("#refund_message")).toContainText(`EVENT_JSON:{"standard":"nep141","version":"1.0.0","event":"ft_transfer","data":[{"old_owner_id":"${contractId}","new_owner_id":"${accountId}","amount":"127999973"}]}\nrefunded 127999973 to ${accountId}`); }); diff --git a/examples/aiproxy/playwright-tests/near_rpc.js b/examples/aiproxy/playwright-tests/near_rpc.js index 9f2ce5b..f98042f 100644 --- a/examples/aiproxy/playwright-tests/near_rpc.js +++ b/examples/aiproxy/playwright-tests/near_rpc.js @@ -1,49 +1,66 @@ -import { Worker } from 'near-workspaces'; +import { KeyPairEd25519, KeyPair, parseNEAR, Worker } from 'near-workspaces'; + import { readFile } from 'fs/promises'; import { createServer } from "http"; import { createHash } from 'crypto'; + const worker = await Worker.init({ port: 14500, rpcAddr: "http://localhost:14500" }); + +process.on('exit', () => { + console.log('Tearing down sandbox worker'); + worker.tearDown(); +}); + const aiTokenAccount = await worker.rootAccount.createAccount('aitoken.test.near'); await aiTokenAccount.deploy(await readFile('../fungibletoken/out/fungible_token.wasm')); await aiTokenAccount.call(aiTokenAccount.accountId, 'new_default_meta', { owner_id: aiTokenAccount.accountId, total_supply: 1_000_000_000_000n.toString() }); +const publicKeyBytes = KeyPair.fromString('ed25519:'+process.env.SPIN_VARIABLE_REFUND_SIGNING_KEY).getPublicKey().data; + const javascript = (await readFile(new URL('../../fungibletoken/e2e/aiconversation.js', import.meta.url))).toString() - .replace("REPLACE_REFUND_SIGNATURE_PUBLIC_KEY", JSON.stringify(Array.from((await aiTokenAccount.getKey()).getPublicKey().data))); + .replace("REPLACE_REFUND_SIGNATURE_PUBLIC_KEY", JSON.stringify(Array.from(publicKeyBytes))); await aiTokenAccount.call( aiTokenAccount.accountId, - 'post_javascript', - {javascript} + 'post_javascript', + { javascript } ); -const alice = await worker.rootAccount.createAccount('alice.test.near'); -await alice.call(aiTokenAccount.accountId, 'storage_deposit', { - account_id: alice.accountId, +const aiuser = await worker.rootAccount.createAccount('aiuser.test.near'); +await aiuser.call(aiTokenAccount.accountId, 'storage_deposit', { + account_id: aiuser.accountId, registration_only: true, }, { attachedDeposit: 1_0000_0000000000_0000000000n.toString() }); await aiTokenAccount.call(aiTokenAccount.accountId, 'ft_transfer', { - receiver_id: alice.accountId, + receiver_id: aiuser.accountId, amount: 128_000_000n.toString(), }, { attachedDeposit: 1n.toString() }); -const real_conversation_id = `alice.test.near_${new Date().getTime()}`;; -const conversation_id = createHash('sha256').update(Buffer.from(real_conversation_id, 'utf8')).digest('hex'); +const functionAccessKeyPair = KeyPairEd25519.fromRandom(); -await alice.call(aiTokenAccount.accountId, 'call_js_func', { - function_name: "start_ai_conversation", - conversation_id +await aiuser.updateAccessKey(functionAccessKeyPair, { + permission: { + FunctionCall: { + method_names: ["call_js_func"], receiver_id: aiTokenAccount.accountId, allowance: parseNEAR("0.25").toString(), + } + }, nonce: 0 }); +const publicKey = (await aiuser.getKey()).getPublicKey().toString(); + const server = createServer((req, res) => { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ - real_conversation_id + publicKey, + functionAccessKeyPair: functionAccessKeyPair.toString(), + accountId: aiuser.accountId, + contractId: aiTokenAccount.accountId })); }); diff --git a/examples/aiproxy/playwright-tests/openaimockserver.js b/examples/aiproxy/playwright-tests/openaimockserver.js index 0ecbb76..5f1ffc7 100644 --- a/examples/aiproxy/playwright-tests/openaimockserver.js +++ b/examples/aiproxy/playwright-tests/openaimockserver.js @@ -17,10 +17,11 @@ const server = createServer((req, res) => { choices: [ { delta: { - content: "I am just a mockserver\n", + content: "Hello! How can I assist you today?\n", }, }, ], + usage: null }), JSON.stringify({ choices: [ @@ -30,6 +31,15 @@ const server = createServer((req, res) => { }, }, ], + usage: + { + "prompt_tokens": 18, + "completion_tokens": 9, + "total_tokens": 27, + "prompt_tokens_details": + { "cached_tokens": 0 }, + "completion_tokens_details": { "reasoning_tokens": 0 } + } }), "[DONE]", ]; diff --git a/examples/aiproxy/playwright.config.js b/examples/aiproxy/playwright.config.js index 3466277..b89aa6c 100644 --- a/examples/aiproxy/playwright.config.js +++ b/examples/aiproxy/playwright.config.js @@ -19,6 +19,7 @@ export default defineConfig({ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: 'http://127.0.0.1:8080', + video: 'off', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -79,7 +80,7 @@ export default defineConfig({ reuseExistingServer: !process.env.CI, }, { - command: "node playwright-tests/near_rpc.js", + command: "export $(grep -v '^#' .test.env | xargs) && node playwright-tests/near_rpc.js", url: 'http://127.0.0.1:14501', reuseExistingServer: !process.env.CI, }], diff --git a/examples/aiproxy/web/index.html b/examples/aiproxy/web/index.html index 90fe237..f00b0c2 100644 --- a/examples/aiproxy/web/index.html +++ b/examples/aiproxy/web/index.html @@ -4,159 +4,111 @@
-+