diff --git a/.github/workflows/docker-nft-ghostnet-staging.yml b/.github/workflows/docker-nft-ghostnet-staging.yml index f01985d..34000c0 100644 --- a/.github/workflows/docker-nft-ghostnet-staging.yml +++ b/.github/workflows/docker-nft-ghostnet-staging.yml @@ -34,8 +34,6 @@ jobs: set -x ls -last . sed -i 's|http://localhost:8000|https://staging.gas-station-api.marigold.dev|g' ./examples/nft/.env - sed -i 's|KT1Re88VMEJ7TLHTkXSHQYZQTD3MP3k7j6Ar|KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx|g' ./examples/nft/.env - sed -i 's|KT1Rp1rgfwS25XrWU6fUnR8cw6KMZBhDvXdq|KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu|g' ./examples/nft/.env cat ./examples/nft/.env - name: Set up Docker Buildx diff --git a/.gitignore b/.gitignore index ffeeff4..d312076 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /examples/nft/node_modules/ /examples/nft/.svelte-kit/ node_modules -dist \ No newline at end of file +dist +**/*d.ts +**/*.js \ No newline at end of file diff --git a/README.md b/README.md index 0892a1f..2030c48 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Tezos Gas Station library -This library helps you -- use [Marigold's Gas Station API](https://github.com/marigold-dev/gas-station) in TypeScript -- create [TZIP-17 permit contracts](https://tzip.tezosagora.org/proposal/tzip-17/), which are FA2 - contracts that can be manipulated by a 3rd party (such as the gas station API). Permits are signed +This library facilitates the following: +- Utilizing [Marigold's Gas Station API](https://github.com/marigold-dev/gas-station) in TypeScript. +- Creating [TZIP-17 permit contracts](https://tzip.tezosagora.org/proposal/tzip-17/), which are FA2 + contracts capable of being manipulated by a 3rd party, such as the gas station API. Permits are signed off-chain and can be posted and executed by anyone. -A toy webapp example is available in the `examples/` directory. +An example of a toy web app is provided in the `examples/nft` directory. -Contributions welcome at https://github.com/marigold-dev/gas-station-lib. +Feel free to contribute and provide feedback on https://github.com/marigold-dev/gas-station-lib. diff --git a/examples/nft/.env b/examples/nft/.env index 60e473d..943432c 100644 --- a/examples/nft/.env +++ b/examples/nft/.env @@ -1,6 +1,6 @@ PUBLIC_TZKT_API=https://api.ghostnet.tzkt.io PUBLIC_TEZOS_RPC=https://ghostnet.smartpy.io -PUBLIC_GAS_STATION_API=http://localhost:8000/operation -PUBLIC_PERMIT=KT1Re88VMEJ7TLHTkXSHQYZQTD3MP3k7j6Ar # GHOSTNET : KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx -PUBLIC_STAKING_CONTRACT=KT1Rp1rgfwS25XrWU6fUnR8cw6KMZBhDvXdq # GHOSTNET : KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu +PUBLIC_GAS_STATION_API=http://localhost:8000 +PUBLIC_PERMIT=KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J +PUBLIC_STAKING_CONTRACT=KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn PUBLIC_APP_BASE_URL=http://localhost:5173 diff --git a/examples/nft/README.md b/examples/nft/README.md index 336b83c..f48502d 100644 --- a/examples/nft/README.md +++ b/examples/nft/README.md @@ -1,5 +1,82 @@ -Toy example of a webapp using a gas station API and a permit contract. The webapp lets you mint a -NFT and transfer it to a “staking” contract even if you have no tez in your wallet. +# Gas Station Demo -The contracts are already deployed on Ghostnet. Their addresses, together with the API URL, can be -changed in the `.env` file. +This project is a toy web application demonstrating the use of a Gas Station API and a Permit Contract on the Tezos blockchain. + +The web app allows you to mint a non-fungible token (NFT) and transfer it to a "staking" contract, even if you have no Tez in your wallet. + + +## Prerequisites + +Before you start, ensure you have the following installed on your machine: + +- Node.js [Download and Install Node.js](https://nodejs.org/) +- npm (Node Package Manager): Comes with Node.js installation +- NVM (Node Version Manager): This is the key component for managing Node.js versions. + To install NVM for managing Node.js versions, refer to the official installation guide + at [install](https://github.com/nvm-sh/nvm#installing-and-updating) + +## Gas Station API and Permit Contract + +This project interacts with a Gas Station API and a Permit Contract on the Tezos blockchain. The contracts are already deloyed on Ghostnet. Configure the follwing in the `.env` file: + +- **Gas Station API URL**: the URL of the Gas Station API that handles the reply of operations to the blockchain. +- **Permit Contract Address**: the address of the Permit Contract on the Tezos blockchain. This contract is used for authorizing and signing operations. +- **Staking Contract Address (Optional)**: If applicable, the address of the Staking Contract where NFTs can be transferred. + +**Note:** The contract addresses and API URL can be changed in the `.env` file. + + +## Configuration + +Copy the `.env.example` file to `.env` and update the following variables based on your deployment: + +```dotenv +PUBLIC_TZKT_API=https://api.ghostnet.tzkt.io +PUBLIC_TEZOS_RPC=https://ghostnet.smartpy.io + +# Gas Station API URL +PUBLIC_GAS_STATION_API=http://localhost:8000 + +# Permit Contract Address +PUBLIC_PERMIT=KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J + +# Staking Contract Address (if applicable) +PUBLIC_STAKING_CONTRACT=KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn + +PUBLIC_APP_BASE_URL=http://localhost:5173 +``` + +## Running locally + +Navigate to the project folder: + +``` +cd examples/nft +``` + +To install dependencies, run: + +``` +npm install +``` +or + +``` +npm i +``` + +To launch the project on your localhost using a development server, run: + +``` +npm run dev +``` + +Your project will be accessible at http://localhost:5173/ + +To clean up generated files, run: + +``` +rm -rf node_modules +``` + +Make sure to configure the `.env` file with the appropriate values before running the project locally. \ No newline at end of file diff --git a/examples/nft/package-lock.json b/examples/nft/package-lock.json index c3fd780..e514360 100644 --- a/examples/nft/package-lock.json +++ b/examples/nft/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@airgap/beacon-sdk": "^4.0.4", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@marigold-dev/gas-station-lib": "^0.0.4", + "@marigold-dev/gas-station-lib": "^0.0.7", "@picocss/pico": "^1.5.10", "@taquito/beacon-wallet": "17.3.0", "@taquito/michel-codec": "17.3.0", @@ -602,9 +602,9 @@ } }, "node_modules/@marigold-dev/gas-station-lib": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@marigold-dev/gas-station-lib/-/gas-station-lib-0.0.4.tgz", - "integrity": "sha512-szNFJyv1943FaBri/RB5k0ctpX31zBUaL4bIEsuRRndN+j0SzkJ/cwEGh6WhKtqKai7VjaS30fpkotwwNix9SQ==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@marigold-dev/gas-station-lib/-/gas-station-lib-0.0.7.tgz", + "integrity": "sha512-RLgm/pNVw9E2prLsMSV425N08P5a0hT6scIcv+PPO23MRZJ+1K6jEB0LFfYD3jvZgy+wzCAkVksl85wwIVVhQw==", "dependencies": { "@taquito/michel-codec": "17.3.0", "@taquito/rpc": "17.3.0", diff --git a/examples/nft/package.json b/examples/nft/package.json index fce4edc..80b20ad 100644 --- a/examples/nft/package.json +++ b/examples/nft/package.json @@ -21,7 +21,7 @@ }, "type": "module", "dependencies": { - "@marigold-dev/gas-station-lib": "^0.0.4", + "@marigold-dev/gas-station-lib": "^0.0.7", "@airgap/beacon-sdk": "^4.0.4", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@picocss/pico": "^1.5.10", diff --git a/examples/nft/src/lib/MintingComponent.svelte b/examples/nft/src/lib/MintingComponent.svelte index 75b61bd..1a6ae3b 100644 --- a/examples/nft/src/lib/MintingComponent.svelte +++ b/examples/nft/src/lib/MintingComponent.svelte @@ -1,78 +1,154 @@
- + {#if isMinting} +
+
+
+ {/if}
{#if user_tokens.length == 0}

You don't have any tokens. Try minting one!

{:else} - {#each user_tokens as token, i} -
- Token thumnail -
{token.balance}
-
- {/each} +
+ {#each user_tokens as token, i} +
+ {#if Object.hasOwn(token.token, "metadata")} + Token thumnail +
+ {token.balance} +
+ {/if} +
+ {/each} +
{/if}
+ \ No newline at end of file diff --git a/examples/nft/src/lib/Nav.svelte b/examples/nft/src/lib/Nav.svelte index de31c0a..e1b1fd7 100644 --- a/examples/nft/src/lib/Nav.svelte +++ b/examples/nft/src/lib/Nav.svelte @@ -1,7 +1,7 @@
- + {#if isStashing} +
+
+
+ {/if}
{#if user_tokens.length == 0} -

You don't have any tokens staked.

+

You don't have any tokens stashed.

{:else} - {#each user_tokens as token, i} -
- Token thumnail -
{token.balance}
-
- {/each} +
+ {#each user_tokens as token, i} +
+ {#if Object.hasOwn(token.token, "metadata")} + Token thumnail +
+ {token.balance} +
+ {/if} +
+ {/each} +
{/if}
- + \ No newline at end of file diff --git a/examples/nft/src/routes/+page.svelte b/examples/nft/src/routes/+page.svelte index 888095b..5fc7616 100644 --- a/examples/nft/src/routes/+page.svelte +++ b/examples/nft/src/routes/+page.svelte @@ -2,32 +2,50 @@ import { myAccount, getPKH, Tezos } from "$lib/tezos"; import MintingComponent from "$lib/MintingComponent.svelte"; import StakingComponent from "$lib/StakingComponent.svelte"; - import { PUBLIC_PERMIT, PUBLIC_STAKING_CONTRACT } from '$env/static/public'; + import { PUBLIC_PERMIT, PUBLIC_STAKING_CONTRACT } from "$env/static/public"; + + // Shared between the two components + let available_token_ids; -

Permit demo

-
- {#if $myAccount == undefined} - You're not connected. - {:else}{#await getPKH() then pkh} -
-

NFTs in your wallet

- -
-
-

Staked NFTs

- -
- {/await} - {/if} -
+
+

Gas Station Demo

+ +

+ This user-friendly dApp is tailored for individuals without any ꜩ in their + wallets. Operations are seamlessly relayed through the Gas Station. +

+ +

+ Minting a new NFT is achieved with a single call to the smart + contract. However, stashing an NFT to another smart contract requires + a transfer, which must be authorized off-chain through the signature of a permit. +

+ +
+ {#if $myAccount === undefined} +

Please connect to use the dApp.

+ {:else}{#await getPKH() then pkh} +
+

NFTs in your wallet

+

{PUBLIC_PERMIT}

+ +
+
+

Stashed NFTs

+

{PUBLIC_STAKING_CONTRACT}

+ +
+ {/await} + {/if} +
+
diff --git a/index.ts b/index.ts index d23b985..ca40b7e 100644 --- a/index.ts +++ b/index.ts @@ -4,15 +4,33 @@ import { packDataBytes } from "@taquito/michel-codec"; import blake2b from "blake2b"; import { hex2buf } from "@taquito/utils"; +/** + * index.ts: Interacting with the Tezos blockchain, allowing users to perform token + * transfers and obtain permits for certain operations. + */ + +/** + * Settings: Describes the settings object expected by the GasStation class, with an apiURL property. + */ + export type Settings = { apiURL: string; }; +/** + * Operation: Represents a generic blockchain operation with destination and parameters properties + */ + export type Operation = { destination: string; parameters: any; }; +/** + * TransferOperation: Represents a specific type of operation for transferring tokens, + * with from_ as the sender's address and an array of transactions (txs) + */ + export type TransferOperation = { from_: string; txs: [ @@ -24,26 +42,52 @@ export type TransferOperation = { ]; }; +/** + * PermitOperation: Represents an operation for obtaining a permit, + * including publicKey, signature, and transferHash properties + */ + export type PermitOperation = { publicKey: string; signature: string; transferHash: string; }; +export const GAS_STATION_PUBLIC_API_GHOSTNET = + "https://ghostnet.gas-station-api.marigold.dev"; + +export const GAS_STATION_PUBLIC_API_MAINNET = + "https://gas-station-api.marigold.dev"; + +/** + * GasStation is responsible for interacting with a remote API to post blockchain operations. + */ + export class GasStation { url: string; - constructor(settings: Settings) { - this.url = settings.apiURL; + /** + * + * @param settings (optional) object + * - apiURL: the URL of Gas Station API. /!\ For this version, the URL must redirect to the endpoint /operation + * + * Takes a Settings object and initializes the url property. + */ + constructor(settings?: Settings) { + this.url = settings?.apiURL || GAS_STATION_PUBLIC_API_GHOSTNET; } + /** + * postOperations: Sends a POST request to the specified API endpoint with the provided operations. + */ + async postOperations(sender: string, ops: Array) { const post_content = { sender_address: sender, operations: ops, }; - const response = await fetch(this.url, { + const response = await fetch(`${this.url}/operation`, { method: "POST", mode: "cors", cache: "no-cache", @@ -57,11 +101,26 @@ export class GasStation { return await response.json(); } + /** + * postOperation: A convenience method to post a single operation using postOperations + */ + postOperation(sender: string, op: Operation) { return this.postOperations(sender, [op]); } } +/** + * PermitContract: interacts with a Tezos smart contract, specifically for + * generating permits related to token transfers. + * + * It uses the Tezos toolkit to interact with the Tezos blockchain, + * fetch contract information, and perform operations. + * + * The code utilizes various Tezos-specific functions and conventions for + * encoding data, hashing, and interacting with smart contracts + */ + export class PermitContract { address: string; tezos: TezosToolkit; @@ -71,12 +130,21 @@ export class PermitContract { this.tezos = tezos; } + /** + * getCounter: Retrieves the counter value from the contract's storage + */ + async getCounter() { const contract = await this.tezos.wallet.at(this.address); // @ts-ignore return (await contract.storage()).extension.counter.c[0]; } + /** + * generatePermit: Generates a permit for a given transfer operation by computing a + * transfer hash and constructing permit data. + */ + async generatePermit(transfer: TransferOperation) { // @ts-ignore const rpcClient = new RpcClient(this.tezos._rpc, "main"); @@ -116,10 +184,15 @@ export class PermitContract { const permit_bytes = packDataBytes(permit_data, permit_type).bytes; console.info("Permit bytes :", permit_bytes); - console.info("Transfert hash : ", transfer_hash); + console.info("Transfer hash : ", transfer_hash); return { bytes: permit_bytes, transfer_hash: transfer_hash }; } + /** + * permitCall: Calls the permit entrypoint on the contract with + * the provided permit operation parameters. + */ + async permitCall(op: PermitOperation) { const contract = await this.tezos.wallet.at(this.address); @@ -127,7 +200,7 @@ export class PermitContract { .permit([[op.publicKey, op.signature, op.transferHash]]) .toTransferParams(); - console.info("Transfert params", call); + console.info("Transfer params", call); return call; } diff --git a/package-lock.json b/package-lock.json index c26bc5a..c716f35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@marigold-dev/gas-station-lib", - "version": "0.0.4", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@marigold-dev/gas-station-lib", - "version": "0.0.4", + "version": "0.0.7", "license": "LGPL 3", "dependencies": { "@taquito/michel-codec": "17.3.0", diff --git a/package.json b/package.json index 63c7145..5446cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@marigold-dev/gas-station-lib", - "version": "0.0.4", + "version": "0.0.8", "description": "Interact with a gas station API and produce TZIP 17 permits", "main": "./dist/index.js", "files": [