Skip to content

Commit

Permalink
FT metadata, web4, docs (#30)
Browse files Browse the repository at this point in the history
Expose the ft_metadata method to the contract wasm. It is needed by wallets to properly show the balance.
Docs about setting up the Fungible Token contract
Implementation of web4_get to be able to serve a web page from the contract
Scripts for bundling for web4, using the AI proxy frontend as an example
Docs about submitting javascript code to the contract
Docs for deploying the AI proxy to Spin cloud
  • Loading branch information
petersalomonsen authored Dec 31, 2024
1 parent 5c554e7 commit 6a9e5b6
Show file tree
Hide file tree
Showing 13 changed files with 680 additions and 44 deletions.
2 changes: 2 additions & 0 deletions examples/aiproxy/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
target
.spin
dist
web4.js
56 changes: 55 additions & 1 deletion examples/aiproxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ There is a simple example of a web client in the [web](./web/) folder.

The application will keep track of of token usage per conversation in the built-in key-value storage of Spin. The initial balance for a conversation is retrieved from the Fungible Token smart contract.

To launch the application, make sure to have the Spin SDK installed. Set the environment variable `SPIN_VARIABLE_OPENAI_API_KEY` to your OpenAI API key.
To launch the application, make sure to have the Spin SDK installed.

You also need to set some environment variables:

- `SPIN_VARIABLE_OPENAI_API_KEY` your OpenAI API key.
- `SPIN_VARIABLE_REFUND_SIGNING_KEY` an ed21159 secret key that will be used to sign refund requests. You can run the [create-refund-signing-keypair.js](./create-refund-signing-keypair.js) script to create the keypair. Run it using the command `$(node create-refund-signing-keypair.js)` and it will set the environment variable for you.
- `SPIN_VARIABLE_FT_CONTRACT_ID` the NEAR contract account id. e.g `aitoken.test.near`
- `SPIN_VARIABLE_OPENAI_COMPLETIONS_ENDPOINT` OpenAI API completions endpoint. E.g. https://api.openai.com/v1/chat/completions
- `SPIN_VARIABLE_RPC_URL` The NEAR RPC node URL. E.g. https://rpc.mainnet.near.org

Then run the following commands:

Expand All @@ -24,3 +32,49 @@ http-server web
```

You will then find the web client at http://localhost:8080. Here you can have a conversation with the AI model.

# Deploying

## Deploying to Spin cloud

While you can deploy to your own Kubernetes cluster using [spinkube](https://www.spinkube.dev/), the easiest approach, that we will describe here is to deploy to the [Fermyon cloud](https://www.fermyon.com/cloud).

You can find a prebuilt image at the [github registry](https://github.com/petersalomonsen/quickjs-rust-near/pkgs/container/near-ft-openai-proxy), and deploy it using the following command:

```bash
spin deploy -f ghcr.io/petersalomonsen/near-ft-openai-proxy:v0.0.2 --variable refund_signing_key=4FGKKSoRmSVu5q8M1w1fuewJSNwKbM2Cw84EDcz3V2eB --variable ft_contract_id=arizcredits.testnet --variable openai_api_key=sk-Q4QE2pIc4LG_aA --variable rpc_url=https://rpc.testnet.near.org --variable openai_completions_endpoint=https://api.openai.com/v1/chat/completions
```

The variables passed in should be adjusted to your setup. Here's an explanation:

- `refund_signing_key` - This is the signing key used by the AI proxy to sign refund requests. The contract needs the corresponding public key to verify signatures from the AI proxy.
- `ft_contract_id` - This is the Fungible Token contract account id
- `openai_api_key` - The API key for accessing the OpenAI completions endpoint
- `rpc_url` - NEAR RPC node URL
- `openai_completions_endpoint` - The OpenAI chat completion endpoint. Can be any OpenAI API compatible URL

## Setting up the Fungible Token contract

To set up the Fungible Token contract to use with the AI proxy, you need to provide initial supply and metadata. Here is an example of how the "ARIZ" token was set up on the testnet.

```bash
near contract call-function as-transaction arizcredits.testnet new json-args '{"owner_id": "arizcredits.testnet", "total_supply": "9999999999999", "metadata": { "spec": "ft-1.0.0","name": "Ariz credits","symbol": "ARIZ","decimals": 6, "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMjI1IDMyMy4zIDkwIDEwMi45IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDIyNSAzMjMuMyA5MCAxMDIuOTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4NCgkuc3Qwe2ZpbGw6IzM3NDUzNjt9DQoJLnN0MXtmaWxsOiNCREM2QjA7fQ0KCS5zdDJ7ZmlsbDojRjJBMzQxO30NCgkuc3Qze29wYWNpdHk6MC43NTtjbGlwLXBhdGg6dXJsKCNTVkdJRF8wMDAwMDEyNjI5Mzk1Njg3OTY0Mjk2Njk5MDAwMDAwNTIyMTQ4ODk5NTI1NDU0MDQ1NF8pO2ZpbGw6I0NFRDhCRjtlbmFibGUtYmFja2dyb3VuZDpuZXcgICAgO30NCgkuc3Q0e29wYWNpdHk6MC43NTtmaWxsOiNFMEM2NjY7ZW5hYmxlLWJhY2tncm91bmQ6bmV3ICAgIDt9DQoJLnN0NXtvcGFjaXR5OjAuNzU7Y2xpcC1wYXRoOnVybCgjU1ZHSURfMDAwMDAwNzIyNjM4MDU2OTM3MzIwNDg4MTAwMDAwMTU0Mzc2NjQ5ODcwODQ1ODE4MTlfKTtmaWxsOiNDRUQ4QkY7ZW5hYmxlLWJhY2tncm91bmQ6bmV3ICAgIDt9DQoJLnN0NntmaWxsOiNGMkYyRjM7fQ0KCS5zdDd7b3BhY2l0eTowLjc1O2NsaXAtcGF0aDp1cmwoI1NWR0lEXzAwMDAwMDg1MjI1MzI1NjAwNDEzOTI3NTIwMDAwMDA2NDcxMjI3Mzc2ODM3Njk2NjY3Xyk7ZmlsbDojQ0VEOEJGO2VuYWJsZS1iYWNrZ3JvdW5kOm5ldyAgICA7fQ0KCS5zdDh7b3BhY2l0eTowLjc1O2NsaXAtcGF0aDp1cmwoI1NWR0lEXzAwMDAwMDQwNTczODM1Mjg3MDI2MTIzNDEwMDAwMDEwMjQzNDkxMDA5NDg5ODI3MjI1Xyk7ZmlsbDojQ0VEOEJGO2VuYWJsZS1iYWNrZ3JvdW5kOm5ldyAgICA7fQ0KCS5zdDl7ZmlsbDojNDE0NDQyO30NCjwvc3R5bGU+DQo8Zz4NCgk8Zz4NCgkJPGc+DQoJCQk8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMzE0LjcsMzIzLjNWMzk5aC0yMC43di03NS42SDMxNC43eiIvPg0KCQkJPHBvbHlnb24gY2xhc3M9InN0MiIgcG9pbnRzPSIzMTQuNywzMjMuMyAyNDkuNSw0MjYuMiAyMjUsNDI2LjIgMjk0LjEsMzIzLjMgCQkJIi8+DQoJCQk8Zz4NCgkJCQk8Zz4NCgkJCQkJPGRlZnM+DQoJCQkJCQk8cG9seWdvbiBpZD0iU1ZHSURfMDAwMDAwMjc1NzY0MTg5NDYyOTQ0NDcxMzAwMDAwMTY0MzY2NzA2MTc4NzAyOTY0NTlfIiBwb2ludHM9IjMxNC43LDMyMy4zIDI0OS41LDQyNi4yIDIyNSw0MjYuMiANCgkJCQkJCQkyOTQuMSwzMjMuMyAJCQkJCQkiLz4NCgkJCQkJPC9kZWZzPg0KCQkJCQk8Y2xpcFBhdGggaWQ9IlNWR0lEXzAwMDAwMDg0NTA0NjkyOTM2NDA2NjQ3MDAwMDAwMDEwNDAyMDQ3MzI3MjgzNDgxNDgxXyI+DQoJCQkJCQk8dXNlIHhsaW5rOmhyZWY9IiNTVkdJRF8wMDAwMDAyNzU3NjQxODk0NjI5NDQ0NzEzMDAwMDAxNjQzNjY3MDYxNzg3MDI5NjQ1OV8iIHN0eWxlPSJvdmVyZmxvdzp2aXNpYmxlOyIvPg0KCQkJCQk8L2NsaXBQYXRoPg0KCQkJCQk8cGF0aCBzdHlsZT0ib3BhY2l0eTowLjc1O2NsaXAtcGF0aDp1cmwoI1NWR0lEXzAwMDAwMDg0NTA0NjkyOTM2NDA2NjQ3MDAwMDAwMDEwNDAyMDQ3MzI3MjgzNDgxNDgxXyk7ZmlsbDojQ0VEOEJGO2VuYWJsZS1iYWNrZ3JvdW5kOm5ldyAgICA7IiBkPSJNMzE0LjcsMzIzLjNWMzk5aC0yMC43di03NS42SDMxNC43eiIvPg0KCQkJCTwvZz4NCgkJCTwvZz4NCgkJPC9nPg0KCQk8cG9seWdvbiBjbGFzcz0ic3Q0IiBwb2ludHM9IjI5NC4xLDMyMy4zIDI5NC4xLDM1NiAzMTQuNywzMjMuMyAJCSIvPg0KCTwvZz4NCjwvZz4NCjwvc3ZnPg0K"}}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as arizcredits.testnet network-config testnet sign-with-keychain send
```

### Submitting the Javascript code

The special functions for the AI conversation and web4 should be posted as javascript code to the contract. Below is an example taking the content of the files [../fungibletoken/e2e/aiconversation.js](../fungibletoken/e2e/aiconversation.js) and [web4.js](./web4.js).

The first command `yarn aiproxy:web4bundle` takes the `index.html` and `main.js` files in the [web](./web/) folder, bundles it and encodes it as base64 in the `web4_get`function response, resulting in the file `web4.js`.

Note when creating the `JSON_ARGS`, that the `aiconversation.js` and `web4.js` files are concatenated and inserted into the `javascript` property of the function call args. In the file [aiconversation.js](../fungibletoken/e2e/aiconversation.js), there is the placeholder `REPLACE_REFUND_SIGNATURE_PUBLIC_KEY`, which needs to be replaced with the public key corresponding to the signing key passed to the AI proxy above. This replacement is also done in the command snippet below.

```bash
export NETWORK_ID=testnet
export RPC_URL=https://rpc.testnet.near.org
export AI_PROXY_BASEURL=https://openai-proxy-zoukmtuw.fermyon.app
yarn aiproxy:web4bundle
export JSON_ARGS=$(cat ../fungibletoken/e2e/aiconversation.js web4.js | sed "s/REPLACE_REFUND_SIGNATURE_PUBLIC_KEY/${REPLACE_REFUND_SIGNATURE_PUBLIC_KEY}/g" | jq -Rs '{javascript: .}')
near contract call-function as-transaction arizcredits.testnet post_javascript json-args $JSON_ARGS prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' sign-as arizcredits.testnet network-config testnet sign-with-keychain send
```

4 changes: 4 additions & 0 deletions examples/aiproxy/create-refund-signing-keypair.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { KeyPairEd25519 } from "near-workspaces";
const keypair = KeyPairEd25519.fromRandom();
console.log(`export REPLACE_REFUND_SIGNATURE_PUBLIC_KEY=${JSON.stringify(Array.from(keypair.getPublicKey().data))}`);
console.log(`export SPIN_VARIABLE_REFUND_SIGNING_KEY=${keypair.secretKey}`);
44 changes: 44 additions & 0 deletions examples/aiproxy/rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import html from '@web/rollup-plugin-html';
import { terser } from 'rollup-plugin-terser';
import { readFileSync, writeFileSync } from 'fs';

const { AI_PROXY_BASEURL, RPC_URL, NETWORK_ID } = process.env;

if (!AI_PROXY_BASEURL) {
throw ('Environment variable AI_PROXY_BASEURL not set. Must be set to the base URL of where the AI proxy is hosted');
}
if (!RPC_URL) {
throw ('Environment variable RPC_URL not set. Must be set to the NEAR RPC node URL');
}
if (!NETWORK_ID) {
throw ('Environment variable NETWORK_ID not set. Must be set to the NEAR protocol network id ( e.g. mainnet, testnet )');
}

export default {
input: ['./web/index.html'],
output: { dir: 'dist' },
plugins: [html({ minify: true }), terser(), {
name: 'inline-js',
closeBundle: () => {
const js = readFileSync('dist/main.js').toString()
.replace('http://localhost:3000', AI_PROXY_BASEURL)
.replace('http://localhost:14500', RPC_URL)
.replace('"sandbox"', `"${NETWORK_ID}"`);

const html = readFileSync('dist/index.html').toString()
.replace(`<script type="module" src="./main.js"></script>`, `<script type="module">${js}</script>`);

writeFileSync('web4.js', `
export function web4_get() {
env.value_return(JSON.stringify({
contentType: 'text/html; charset=UTF-8',
body: '${Buffer.from(html).toString('base64')}'
})
);
}
`);

}
}],
};
2 changes: 1 addition & 1 deletion examples/aiproxy/spin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ component = "openai-proxy"

[component.openai-proxy]
source = "openai-proxy/target/wasm32-wasi/release/openai_proxy.wasm"
allowed_outbound_hosts = ["https://api.openai.com:443", "https://aitoken.testnet.page:443", "https://rpc.mainnet.near.org:443", "http://localhost:14500", "http://127.0.0.1:3001"]
allowed_outbound_hosts = ["https://api.openai.com:443", "https://rpc.testnet.near.org:443", "https://rpc.mainnet.near.org:443", "http://localhost:14500", "http://127.0.0.1:3001"]
key_value_stores = ["default"]

[component.openai-proxy.build]
Expand Down
35 changes: 21 additions & 14 deletions examples/aiproxy/web/index.html
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenAI Proxy Streaming</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenAI Proxy Streaming</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>

<body>
<div class="container">
<div class="mb-3">
<label for="conversation_id" class="form-label">Conversation id ( generated )</label>
<input id="conversation_id" class="form-control" readonly />
</div>
<button id="startConversationButton" class="btn btn-primary">Start conversation</button>

<div id="messages"></div>
<div class="mb-3">
<label for="question" class="form-label">Question</label>
<textarea id="question" class="form-control" disabled rows="4" placeholder="Type your question here..."></textarea>
<button id="askAIButton" class="btn btn-primary" disabled>Ask AI</button>
<label for="question" class="form-label">Question</label>
<textarea id="question" class="form-control" disabled rows="4"
placeholder="Type your question here..."></textarea>
<button id="askAIButton" class="btn btn-primary" disabled>Ask AI</button>
</div>
<button id="refundButton" class="btn btn-primary">Stop conversation and refund tokens</button><br />
<div style="width: 100%">
<pre>
<pre>
<code id="refund_message" style="white-space: wrap;"></code>
</pre>
</div>
</div>
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js" crossorigin="anonymous"></script>
<script type="importmap">
<script async src="https://ga.jspm.io/npm:[email protected]/dist/es-module-shims.js"
crossorigin="anonymous"></script>
<script type="importmap">
{
"imports": {
"near-api-js": "https://ga.jspm.io/npm:[email protected]/lib/browser-index.js"
Expand Down Expand Up @@ -107,8 +112,10 @@
}
}
</script>
<script src="main.js" type="module">
</script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="main.js" type="module"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"></script>
</body>

</html>
22 changes: 15 additions & 7 deletions examples/aiproxy/web/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const near = await connect({
});
const walletConnection = new WalletConnection(near, "aiproxy");

const baseUrl = 'http://localhost:3000';
const proxyUrl = `${baseUrl}/proxy-openai`; // Replace with your actual Spin proxy URL
const baseUrl = 'http://localhost:3000'; // Replace with your actual Spin proxy URL
const proxyUrl = `${baseUrl}/proxy-openai`;
let conversation = [
{ role: 'system', content: 'You are a helpful assistant.' }
];
Expand Down Expand Up @@ -45,7 +45,6 @@ async function refund() {

async function startConversation() {
const conversation_id = `${walletConnection.getAccountId()}_${new Date().getTime()}`;
document.getElementById('conversation_id').value = conversation_id;
const conversation_id_hash = Array.from(new Uint8Array(
await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(conversation_id))
))
Expand All @@ -62,9 +61,17 @@ async function startConversation() {
conversation_id: conversation_id_hash
}
});
console.log(result);
document.getElementById('question').disabled = false;
document.getElementById('askAIButton').disabled = false;
localStorage.setItem('conversation_id', conversation_id);
checkExistingConversationId();
}

function checkExistingConversationId() {
const existingConversationId = localStorage.getItem('conversation_id');
if (existingConversationId) {
document.getElementById('conversation_id').value = existingConversationId;
document.getElementById('question').disabled = false;
document.getElementById('askAIButton').disabled = false;
}
}

function escapeHtml(unsafe) {
Expand Down Expand Up @@ -156,4 +163,5 @@ async function sendQuestion() {

document.getElementById('startConversationButton').addEventListener('click', () => startConversation());
document.getElementById('refundButton').addEventListener('click', () => refund());
document.getElementById('askAIButton').addEventListener('click', () => sendQuestion());
document.getElementById('askAIButton').addEventListener('click', () => sendQuestion());
checkExistingConversationId();
33 changes: 33 additions & 0 deletions examples/fungibletoken/e2e/e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,37 @@ describe('Fungible token contract', { only: true }, () => {

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

test('should support web4', { 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 web4_get() {
const request = JSON.parse(env.input()).request;
let response;
if (request.path == '/index.html') {
response = {
contentType: 'text/html; charset=UTF-8',
body: env.base64_encode('<html><body>hello</body></html>')
};
}
env.value_return(JSON.stringify(response));
}
`
}
});

const web4Response = await contract.view('web4_get', { request: { path: '/index.html' } });
expect(web4Response.contentType).to.equal("text/html; charset=UTF-8");
expect(web4Response.body).to.equal(Buffer.from('<html><body>hello</body></html>').toString('base64'));
});
});
10 changes: 10 additions & 0 deletions examples/fungibletoken/meta-dce.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"export": "ft_transfer",
"root": true
},
{
"name": "ft_metadata",
"export": "ft_metadata",
"root": true
},
{
"name": "ft_balance_of",
"export": "ft_balance_of",
Expand Down Expand Up @@ -69,6 +74,11 @@
"export": "storage_balance_of",
"root": true
},
{
"name": "web4_get",
"export": "web4_get",
"root": true
},
{
"name": "new",
"export": "new",
Expand Down
45 changes: 45 additions & 0 deletions examples/fungibletoken/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ impl Contract {
self.store_js_bytecode(compile_js(javascript, Some("main.js".to_string())));
}

pub fn web4_get(&self) {
let jsmod = self.load_js_bytecode();
let web4_get_str = CString::new("web4_get").unwrap();
unsafe {
self.add_js_functions();
js_call_function(jsmod, web4_get_str.as_ptr() as i32);
}
}

fn on_account_closed(&mut self, account_id: AccountId, balance: u128) {
log!("Closed @{} with {}", account_id, balance);
}
Expand Down Expand Up @@ -460,4 +469,40 @@ mod tests {
assert_eq!(contract.ft_balance_of(bob()).0, TOTAL_SUPPLY - 2_000);
assert_eq!(contract.ft_balance_of(alice()).0, 2_000);
}

#[test]
fn test_web4_get() {
setup_test_env();
set_current_account_id(bob());
set_predecessor_account_id(bob());

set_input(
"{\"request\": {\"path\": \"/index.html\"}}"
.try_into()
.unwrap(),
);
let mut contract = Contract::new_default_meta(bob().into(), TOTAL_SUPPLY.into());

contract.post_javascript(
"export function web4_get() {
const request = JSON.parse(env.input()).request;
let response;
if (request.path == '/index.html') {
response = {
contentType: 'text/html; charset=UTF-8',
body: env.base64_encode('<html><body>hello</body></html>')
};
}
env.value_return(JSON.stringify(response));
}"
.to_string(),
);
contract.web4_get();
assert_latest_return_value_string_eq(
r#"{"contentType":"text/html; charset=UTF-8","body":"PGh0bWw+PGJvZHk+aGVsbG88L2JvZHk+PC9odG1sPg=="}"#
.to_owned(),
);
}
}
4 changes: 2 additions & 2 deletions examples/nft/web4/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import html from '@web/rollup-plugin-html';
import { rollupPluginHTML as html } from '@web/rollup-plugin-html';
import { terser } from 'rollup-plugin-terser';
import { readFileSync, readdirSync, unlinkSync, writeFileSync } from 'fs';
import { readFileSync, unlinkSync, writeFileSync } from 'fs';

export default {
input: ['./index.html'],
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,21 @@
"test-quickjslib": "cd quickjslib && ./build.sh && node --test",
"examples-nft-web4bundle": "cd examples/nft/web4 && rollup -c rollup.config.js",
"serve-examples-nft": "http-server -p 8085 examples/nft/web4",
"test-playwright-aiproxy": "yarn playwright test -c examples/aiproxy/playwright.config.js"
"test-playwright-aiproxy": "yarn playwright test -c examples/aiproxy/playwright.config.js",
"aiproxy:web4bundle": "cd examples/aiproxy && rollup -c rollup.config.js"
},
"jest": {
"transform": {}
},
"devDependencies": {
"@playwright/test": "^1.49.1",
"@types/node": "^22.10.2",
"@web/rollup-plugin-html": "^1.11.0",
"@web/rollup-plugin-html": "^2.3.0",
"chai": "^5.1.1",
"http-server": "^14.1.1",

"near-workspaces": "4.0.0",
"rollup": "^2.79.0",
"rollup": "^4.29.1",
"rollup-plugin-terser": "^7.0.2"
},
"dependencies": {}
Expand Down
Loading

0 comments on commit 6a9e5b6

Please sign in to comment.