From b26d701b9263f0cebc84eb437ae582b02755445c Mon Sep 17 00:00:00 2001 From: dwertent Date: Fri, 3 Jan 2025 15:25:32 -0500 Subject: [PATCH 1/7] Add HelloWorld example with Solidity contract and TypeScript integration Signed-off-by: dwertent --- doc-site/docs/tutorials/bond-issuance.md | 2 +- doc-site/docs/tutorials/hello-world.md | 137 ++++++++ doc-site/docs/tutorials/index.md | 52 ++- doc-site/docs/tutorials/notarized-tokens.md | 135 ++++++++ doc-site/docs/tutorials/private-storage.md | 147 +++++++++ doc-site/docs/tutorials/public-storage.md | 131 ++++++++ doc-site/docs/tutorials/zkp-cbdc.md | 8 +- doc-site/mkdocs.yml | 3 + example/bond/src/index.ts | 2 - example/helloworld/.gitignore | 3 + example/helloworld/.vscode/launch.json | 13 + example/helloworld/README.md | 43 +++ example/helloworld/build.gradle | 73 ++++ example/helloworld/package-lock.json | 312 ++++++++++++++++++ example/helloworld/package.json | 23 ++ example/helloworld/scripts/abi.mjs | 6 + example/helloworld/src/index.ts | 97 ++++++ example/helloworld/tsconfig.json | 13 + example/notarized-tokens/.gitignore | 3 + example/notarized-tokens/.vscode/launch.json | 13 + example/notarized-tokens/README.md | 36 ++ example/notarized-tokens/build.gradle | 62 ++++ example/notarized-tokens/package-lock.json | 288 ++++++++++++++++ example/notarized-tokens/package.json | 23 ++ example/notarized-tokens/src/index.ts | 85 +++++ example/notarized-tokens/tsconfig.json | 13 + example/privacy-storage/.gitignore | 3 + example/privacy-storage/.vscode/launch.json | 13 + example/privacy-storage/README.md | 46 +++ example/privacy-storage/build.gradle | 73 ++++ example/privacy-storage/package-lock.json | 312 ++++++++++++++++++ example/privacy-storage/package.json | 23 ++ example/privacy-storage/scripts/abi.mjs | 6 + .../privacy-storage/src/helpers/storage.ts | 36 ++ example/privacy-storage/src/index.ts | 95 ++++++ example/privacy-storage/src/util.ts | 32 ++ example/privacy-storage/tsconfig.json | 13 + example/public-storage/.gitignore | 3 + example/public-storage/.vscode/launch.json | 13 + example/public-storage/README.md | 42 +++ example/public-storage/build.gradle | 73 ++++ example/public-storage/package-lock.json | 312 ++++++++++++++++++ example/public-storage/package.json | 23 ++ example/public-storage/scripts/abi.mjs | 6 + example/public-storage/src/index.ts | 90 +++++ example/public-storage/tsconfig.json | 13 + sdk/typescript/src/domains/pente.ts | 25 +- settings.gradle | 4 + solidity/contracts/tutorials/HelloWorld.sol | 22 ++ solidity/contracts/tutorials/Storage.sol | 28 ++ 50 files changed, 3004 insertions(+), 25 deletions(-) create mode 100644 doc-site/docs/tutorials/hello-world.md create mode 100644 doc-site/docs/tutorials/notarized-tokens.md create mode 100644 doc-site/docs/tutorials/private-storage.md create mode 100644 doc-site/docs/tutorials/public-storage.md create mode 100644 example/helloworld/.gitignore create mode 100644 example/helloworld/.vscode/launch.json create mode 100644 example/helloworld/README.md create mode 100644 example/helloworld/build.gradle create mode 100644 example/helloworld/package-lock.json create mode 100644 example/helloworld/package.json create mode 100644 example/helloworld/scripts/abi.mjs create mode 100644 example/helloworld/src/index.ts create mode 100644 example/helloworld/tsconfig.json create mode 100644 example/notarized-tokens/.gitignore create mode 100644 example/notarized-tokens/.vscode/launch.json create mode 100644 example/notarized-tokens/README.md create mode 100644 example/notarized-tokens/build.gradle create mode 100644 example/notarized-tokens/package-lock.json create mode 100644 example/notarized-tokens/package.json create mode 100644 example/notarized-tokens/src/index.ts create mode 100644 example/notarized-tokens/tsconfig.json create mode 100644 example/privacy-storage/.gitignore create mode 100644 example/privacy-storage/.vscode/launch.json create mode 100644 example/privacy-storage/README.md create mode 100644 example/privacy-storage/build.gradle create mode 100644 example/privacy-storage/package-lock.json create mode 100644 example/privacy-storage/package.json create mode 100644 example/privacy-storage/scripts/abi.mjs create mode 100644 example/privacy-storage/src/helpers/storage.ts create mode 100644 example/privacy-storage/src/index.ts create mode 100644 example/privacy-storage/src/util.ts create mode 100644 example/privacy-storage/tsconfig.json create mode 100644 example/public-storage/.gitignore create mode 100644 example/public-storage/.vscode/launch.json create mode 100644 example/public-storage/README.md create mode 100644 example/public-storage/build.gradle create mode 100644 example/public-storage/package-lock.json create mode 100644 example/public-storage/package.json create mode 100644 example/public-storage/scripts/abi.mjs create mode 100644 example/public-storage/src/index.ts create mode 100644 example/public-storage/tsconfig.json create mode 100644 solidity/contracts/tutorials/HelloWorld.sol create mode 100644 solidity/contracts/tutorials/Storage.sol diff --git a/doc-site/docs/tutorials/bond-issuance.md b/doc-site/docs/tutorials/bond-issuance.md index 004e88c3e..7b4c2d1cf 100644 --- a/doc-site/docs/tutorials/bond-issuance.md +++ b/doc-site/docs/tutorials/bond-issuance.md @@ -8,7 +8,7 @@ This shows how to leverage the [Noto](../../architecture/noto/) and [Pente](../. ## Running the example -Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paldin environment, and +Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paladin environment, and then follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/bond/README.md) to run the code. diff --git a/doc-site/docs/tutorials/hello-world.md b/doc-site/docs/tutorials/hello-world.md new file mode 100644 index 000000000..bcc1ce0ab --- /dev/null +++ b/doc-site/docs/tutorials/hello-world.md @@ -0,0 +1,137 @@ +# Hello World with Paladin + +This tutorial walks you through deploying and interacting with a simple `HelloWorld` smart contract using the Paladin SDK. The example demonstrates how to: +1. Deploy the contract, +2. Interact with it by calling its `sayHello` function, +3. Retrieve and verify the emitted event. + +--- + +## Running the Example + +The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/public-storage). + +The storage solidity contract can be found [here](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). + +Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paladin environment. Then, follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/helloworld/README.md) to run the code. + +--- + +### Overview + +We have a `HelloWorld` smart contract, which: +- Emits a "welcome" message as an event when its `sayHello` function is called. + +### Key Artifacts +To deploy and interact with the contract, we use: +1. **ABI**: Describes the contract's interface, including its functions and events. +2. **Bytecode**: The compiled contract code. + +These are pre-compiled and provided in the `helloWorldJson` object. + +--- + +### Step 1: Deploy the Contract + +```typescript +const deploymentTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Deploy publicly + abi: helloWorldJson.abi, // ABI of the HelloWorld contract + bytecode: helloWorldJson.bytecode, // Compiled bytecode + function: "", // No constructor arguments + from: owner.lookup, // Account signing the transaction + data: {}, // No additional data +}); +``` + +- **What happens**: + - The `sendTransaction` method sends a deployment transaction to the Paladin network. + - The function returns a `deploymentTxID` that uniquely identifies the transaction. + +### Step 2: Confirm the Deployment + +```typescript +const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000, true); +if (!deploymentReceipt?.contractAddress) { + logger.error("Deployment failed!"); + return false; +} +logger.log("Contract deployed successfully at address:", deploymentReceipt.contractAddress); +``` + +- **What happens**: + - We use `pollForReceipt` to wait for the deployment transaction to be confirmed. + - If successful, the receipt includes the new `contractAddress`. + +--- + +### Step 3: Call the `sayHello` Function + +```typescript +const name = "Blocky McChainface"; // Example name for the greeting + +const sayHelloTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, + abi: helloWorldJson.abi, + function: "sayHello", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: { name: name }, +}); +``` + +- **What happens**: + - The `sendTransaction` method sends a transaction to call the `sayHello` function of the deployed contract. + - The `data` object includes the function arguments—in this case, the `name` of the person being greeted. + +--- + +### Step 4: Confirm the Function Call + +```typescript +const functionReceipt = await paladin.pollForReceipt(sayHelloTxID, 10000, true); +if (!functionReceipt?.transactionHash) { + logger.error("Receipt retrieval failed!"); + return false; +} +logger.log("sayHello function executed successfully!"); +``` + +- **What happens**: + - Similar to the deployment step, we wait for confirmation of the `sayHello` function call using `pollForReceipt`. + +--- + +### Step 5: Retrieve the Emitted Event + +```typescript +const events = await paladin.decodeTransactionEvents( + functionReceipt.transactionHash, + helloWorldJson.abi, + "pretty=true", +); + +logger.log(events[0].data["message"]); +``` + +- **What happens**: + - We use `decodeTransactionEvents` to extract event data from the `sayHello` transaction. + +--- + +## Conclusion + +Congratulations! You've successfully: +1. Deployed the `HelloWorld` contract, +2. Called its `sayHello` function, +3. Retrieved and validated the emitted event. + +This simple example demonstrates how to interact with smart contracts using the Paladin SDK. + +--- + +## Next Steps + +Now that you’ve deployed and interacted with the `HelloWorld` contract, you’re ready to explore more complex interactions with smart contracts. In the next tutorial, we will introduce you to a **Storage** contract where you will write and read from from the blockchain! + +[Continue to the Storage Contract Tutorial →](./public-storage.md) diff --git a/doc-site/docs/tutorials/index.md b/doc-site/docs/tutorials/index.md index 738e47381..d7803e7d0 100644 --- a/doc-site/docs/tutorials/index.md +++ b/doc-site/docs/tutorials/index.md @@ -1,9 +1,10 @@ -## Pre-requisites +## Prerequisites -* git -* [NodeJS 20.x or newer](https://nodejs.org/en/download/package-manager) installed +- **Git** +- **Node.js 20.x or newer** + Follow the [official Node.js documentation](https://nodejs.org/en/download/package-manager) for your platform to install the appropriate version. -Clone the Paladin repository to access the examples: +To access the tutorial code, clone the Paladin repository: ```shell git clone https://github.com/LF-Decentralized-Trust-labs/paladin.git @@ -11,24 +12,45 @@ git clone https://github.com/LF-Decentralized-Trust-labs/paladin.git ## Next Steps +The tutorials on this page provide an introduction to building on the Paladin platform. If you haven’t already, visit the [Getting Started guide](../getting-started/installation.md) to familiarize yourself with running Paladin before proceeding with any of the tutorials below. -The tutorials on this page provide a starting point for running code on top of Paladin. +
-If you haven't already, you should visit [Getting Started](../getting-started/installation/) to -ensure you're familiar with running Paladin before walking through any of the tutorials. +- :fontawesome-solid-rocket:{ .lg .middle } **[Hello World](hello-world.md)** + + --- + + Begin with a simple “Hello World” example to get familiar with some of the basics. + +- :fontawesome-solid-rocket:{ .lg .middle } **[Public Storage](public-storage.md)** + + --- + + Explore fundamental SDK functionality for deploying and interacting with a publicly visible contract. + +- :fontawesome-solid-rocket:{ .lg .middle } **[Private Storage](private-storage.md)** + + --- + + Discover how to use **Privacy Groups** and keep contract data confidential among authorized members. + +- :fontawesome-solid-rocket:{ .lg .middle } **[Notarized Tokens](notarized-tokens.md)** + + --- + + Learn how to issue, mint, and transfer tokens using Paladin’s **Notarized Tokens** domain. -
-- :fontawesome-solid-stamp:{ .lg .middle } __[Bond Issuance](bond-issuance.md)__ +- :fontawesome-solid-stamp:{ .lg .middle } **[Wholesale CBDC](zkp-cbdc.md)** - --- + --- - Learn how Noto and Pente work together to represent a bond issuance process. + Implement a wholesale CBDC with **zero-knowledge proof** features for enhanced privacy and regulatory compliance. -- :fontawesome-solid-stamp:{ .lg .middle } __[Wholesale CBDC](zkp-cbdc.md)__ +- :fontawesome-solid-stamp:{ .lg .middle } **[Bond Issuance](bond-issuance.md)** - --- + --- - Learn how to implement a wholesale CBDC with Zeto. + Understand how **Notarized Tokens** and **Privacy Groups** work together to model and manage a bond issuance process. -
+
\ No newline at end of file diff --git a/doc-site/docs/tutorials/notarized-tokens.md b/doc-site/docs/tutorials/notarized-tokens.md new file mode 100644 index 000000000..45cfa8519 --- /dev/null +++ b/doc-site/docs/tutorials/notarized-tokens.md @@ -0,0 +1,135 @@ +# Notarized Tokens + +In this tutorial, you’ll learn how to create and manage a basic token using the **Notarized Tokens (noto)** domain. Unlike the private storage example, these tokens can be transferred between nodes publicly, demonstrating how assets (e.g., “cash”) can be issued and tracked on the blockchain using Paladin. + +## Prerequisites + +Make sure you have: + +1. Completed the [Private Storage Tutorial](./private-storage.md). +2. A **running Paladin network** with at least three nodes (Node1, Node2, Node3). + +--- + +## Overview + +This tutorial will guide you through: + +1. **Deploying a Noto Token**: Use the `NotoFactory` to create a “cash token” with a specific notary. +2. **Minting Tokens**: Issue tokens to a particular node’s account. +3. **Transferring Tokens**: Send tokens between different nodes to simulate basic payments. + +You can find the complete example code in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/tree/main/example/notarized-tokens). + +--- + +## Step 1: Deploy a Noto Token + +First, create a **Noto Factory** instance and deploy a new token. In this scenario, Node1 will act as both the notary (the entity allowed to mint tokens) and the initial recipient of the minted cash. + +```typescript +logger.log("Step 1: Deploying a Noto cash token..."); +const notoFactory = new NotoFactory(paladinClientNode1, "noto"); +const cashToken = await notoFactory.newNoto(verifierNode1, { + notary: verifierNode1, // The notary for this token + restrictMinting: true, // Restrict minting to the notary only +}); +if (!cashToken) { + logger.error("Failed to deploy the Noto cash token!"); + return false; +} +logger.log("Noto cash token deployed successfully!"); +``` + +**Key Points**: +- **`notary`**: Specifies which verifier (account) can mint new tokens. +- **`restrictMinting`**: If `true`, only the `notary` can mint additional tokens. + +--- + +## Step 2: Mint Tokens + +Now that the token contract exists, **mint** an initial supply of tokens to Node1. This step simulates creating new “cash” in the system. + +```typescript +logger.log("Step 2: Minting 2000 units of cash to Node1..."); +const mintReceipt = await cashToken.mint(verifierNode1, { + to: verifierNode1, // Mint cash to Node1 + amount: 2000, // Amount to mint + data: "0x", // Additional data (optional) +}); +if (!mintReceipt) { + logger.error("Failed to mint cash tokens!"); + return false; +} +logger.log("Successfully minted 2000 units of cash to Node1!"); +``` + +**Key Points**: +- **`amount`**: Number of tokens to create. +- **`data`**: Can include extra metadata or encoding, if needed. + +--- + +## Step 3: Transfer Tokens to Node2 + +With tokens minted on Node1, you can **transfer** some of them to Node2. This step demonstrates a simple token transfer, much like sending money to another account. + +```typescript +logger.log("Step 3: Transferring 1000 units of cash from Node1 to Node2..."); +const transferToNode2 = await cashToken.transfer(verifierNode1, { + to: verifierNode2, // Transfer to Node2 + amount: 1000, // Amount to transfer + data: "0x", // Optional additional data +}); +if (!transferToNode2) { + logger.error("Failed to transfer cash to Node2!"); + return false; +} +logger.log("Successfully transferred 1000 units of cash to Node2!"); +``` + +--- + +## Step 4: Transfer Tokens to Node3 + +Now let’s see how Node2 can pass tokens to Node3. This step involves calling `.using(paladinClientNode2)` so that **Node2** signs the transaction rather than Node1. + +```typescript +logger.log("Step 4: Transferring 800 units of cash from Node2 to Node3..."); +const transferToNode3 = await cashToken.using(paladinClientNode2).transfer(verifierNode2, { + to: verifierNode3, // Transfer to Node3 + amount: 800, // Amount to transfer + data: "0x", // Optional additional data +}); +if (!transferToNode3) { + logger.error("Failed to transfer cash to Node3!"); + return false; +} +logger.log("Successfully transferred 800 units of cash to Node3!"); +``` + +**Key Points**: +- **`.using(paladinClientNode2)`** ensures the transaction is signed by Node2. +- If Node2 does not have sufficient tokens (e.g., tries to transfer 1200 while only having 1000), the transfer should fail and return an error. + +--- + +## Conclusion + +Congratulations! You’ve successfully: + +1. **Deployed a Noto token** to represent cash within the Paladin network. +2. **Minted tokens** from a designated notary account. +3. **Transferred tokens** between different nodes, demonstrating how digital assets move across participants. + +At this point, you have a basic grasp of how to issue and manage tokens using the Noto domain. + +--- + +## Next Steps + +Now that you’ve explored how to create, mint, and transfer tokens using the Noto domain, you’re ready to delve into Zeto, Paladin’s zero-knowledge domain for more advanced privacy features. In the next tutorial, you’ll learn how to build a cash payment solution—for example, a wholesale CBDC or a commercial bank money rail—while leveraging powerful privacy techniques such as private minting and selective disclosure. + +[Continue to the Zero-Knowledge Proof Tutorial →](./zkp-cbdc.md) + diff --git a/doc-site/docs/tutorials/private-storage.md b/doc-site/docs/tutorials/private-storage.md new file mode 100644 index 000000000..a39e1526f --- /dev/null +++ b/doc-site/docs/tutorials/private-storage.md @@ -0,0 +1,147 @@ +# Private Storage Contract + +In this tutorial, you'll learn how to deploy and interact with a **private storage contract** using Paladin's privacy groups. Unlike the public storage example, here only authorized members of a privacy group can interact with the contract, ensuring secure and private data handling. + +--- + +## Prerequisites + +Before starting, make sure you have: + +1. Completed the [Public Storage Tutorial](./public-storage.md) and are familiar with: + - Deploying and interacting with contracts. + - Using Paladin SDK for blockchain transactions. +2. A running Paladin network with multiple nodes (at least 3 for this tutorial). + +--- + +## Overview + +The `PrivateStorage` tutorial demonstrates how to: + +1. Create a **privacy group** with selected members. +2. Deploy a **private Storage contract** within the group. +3. Interact with the contract securely within the group. +4. Test privacy by attempting access from a non-member node. + +The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/private-storage). + +The Solidity contract remains the same as in the [Public Storage Tutorial](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). However, the interaction is scoped to the privacy group. + +--- + +## Step 1: Create a Privacy Group + +To enable private interactions, start by creating a privacy group with selected members. + +```typescript +// Create a privacy group with Node1 and Node2 +logger.log("Creating a privacy group for Node1 and Node2..."); +const penteFactory = new PenteFactory(paladinNode1, "pente"); +const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { + group: { + salt: newGroupSalt(), // Generate a unique salt for the group + members: [verifierNode1, verifierNode2], // Include Node1 and Node2 as members + }, + evmVersion: "shanghai", + endorsementType: "group_scoped_identities", + externalCallsEnabled: true, +}); + +if (!checkDeploy(memberPrivacyGroup)) { + logger.error("Failed to create the privacy group."); + return false; +} +logger.log("Privacy group created successfully!"); +``` + +--- + +## Step 2: Deploy the Contract in the Privacy Group + +Deploy the `Storage` contract within the created privacy group. + +```typescript +logger.log("Deploying a private Storage contract..."); +const contractAddress = await memberPrivacyGroup.deploy( + storageJson.abi, // ABI of the Storage contract + storageJson.bytecode, // Bytecode of the Storage contract + verifierNode1 // Deploying as Node1 +); + +if (!contractAddress) { + logger.error("Failed to deploy the private Storage contract."); + return false; +} +logger.log(`Private Storage contract deployed! Address: ${contractAddress}`); +``` + +--- + +## Step 3: Store and Retrieve Values as Group Members + +### Store a Value + +Group members can store values securely in the private contract. + +```typescript +const privateStorage = new PrivateStorage(memberPrivacyGroup, contractAddress); + +logger.log("Storing value (125) in the private Storage contract..."); +const storeTx = await privateStorage.invoke(verifierNode1, "store", { num: 125 }); +logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); +``` + +--- + +### Retrieve the Value as a Member + +Authorized group members can retrieve the stored value. + +```typescript +logger.log("Node1 retrieving the stored value..."); +const retrievedValueNode1 = await privateStorage.call(verifierNode1, "retrieve", []); +logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); + +logger.log("Node2 retrieving the stored value..."); +const retrievedValueNode2 = await privateStorage + .using(paladinNode2) + .call(verifierNode2, "retrieve", []); +logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); +``` + +--- + +## Step 4: Verify Privacy by Testing Unauthorized Access + +When an outsider (Node3) tries to access the private contract, the attempt should fail. + +```typescript +try { + logger.log("Node3 (outsider) attempting to retrieve the value..."); + await privateStorage.using(paladinNode3).call(verifierNode3, "retrieve", []); + logger.error("Node3 (outsider) should not have access to the private Storage contract!"); + return false; +} catch (error) { + logger.info("Node3 (outsider) cannot retrieve data. Access denied."); +} +``` + +--- + +## Conclusion + +Congratulations! You’ve successfully: + +1. Created a privacy group with selected members. +2. Deployed a `Storage` contract in the privacy group. +3. Ensured secure interactions with the contract for group members. +4. Verified that unauthorized access is blocked. + +--- + +## Next Steps + +After exploring Private Storage and learning how to keep contract data confidential within a privacy group, you’re ready to explore other Paladin domains. In the next tutorial, you’ll learn about **Notarized Tokens (Noto)** - a way to create, mint, and transfer tokens on the Paladin network. This will introduce concepts like notaries, restricted minting, and token transfers among multiple nodes. + +[Continue to the Notarized Tokens Tutorial →](./notarized-tokens.md) diff --git a/doc-site/docs/tutorials/public-storage.md b/doc-site/docs/tutorials/public-storage.md new file mode 100644 index 000000000..f05b4165d --- /dev/null +++ b/doc-site/docs/tutorials/public-storage.md @@ -0,0 +1,131 @@ +# Public Storage Contract + +In the [previous tutorial](./hello-world.md), we deployed and interacted with a **HelloWorld** contract that emitted an event. Now, we will take it a step further by deploying a **Storage** contract that: +1. Allows you to store a value on the blockchain. +2. Lets you retrieve the stored value. + +--- + +## Prerequisites + +- You’ve completed the [HelloWorld Tutorial](./hello-world.md) and are familiar with: + - Deploying contracts with the Paladin SDK. + - Sending transactions and retrieving their receipts. + +--- + +## Overview + +The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/public-storage). + +The storage solidity contract can be found [here](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). + +The `Storage` contract provides two main functions: +1. **`store(uint256 num)`**: Stores a value in the contract. +2. **`retrieve()`**: Retrieves the last stored value. + + +### Step 1: Deploy the Contract + +The first step is to deploy the `Storage` contract to the blockchain. + +```typescript +const deploymentTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public deployment + abi: storageJson.abi, // ABI of the Storage contract + bytecode: storageJson.bytecode, // Compiled bytecode + function: "", // No constructor arguments + from: owner.lookup, // Account signing the transaction + data: {}, // No additional data +}); + +const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000); +if (!deploymentReceipt?.contractAddress) { + logger.error("Deployment failed!"); + return false; +} +logger.log("Step 1: Storage contract deployed successfully!"); +``` + +- **What happens**: + - The `sendTransaction` function creates a deployment transaction for the `Storage` contract. + - The `pollForReceipt` function waits for the transaction to be confirmed. + - On success, the contract address is returned in the receipt. + +--- + +### Step 2: Store a Value + +After deploying the contract, you can store a value in the contract using its `store` function. + +```typescript +const valueToStore = 125; // Example value to store +logger.log(`Step 2: Storing value "${valueToStore}" in the contract...`); + +const storeTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public transaction + abi: storageJson.abi, // ABI of the Storage contract + function: "store", // Name of the function to call + from: owner.lookup, // Account signing the transaction + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: { num: valueToStore }, // Function arguments +}); + +const storeReceipt = await paladin.pollForReceipt(storeTxID, 10000); +if (!storeReceipt?.transactionHash) { + logger.error("Failed to store value in the contract!"); + return false; +} +logger.log("Step 2: Value stored successfully!"); +``` + +- **What happens**: + - The `sendTransaction` function sends a transaction to call the `store` function with the value to store (`125` in this example). + - The `pollForReceipt` function waits for the transaction to be confirmed. + +--- + +### Step 3: Retrieve the Stored Value + +Next, retrieve the stored value using the `retrieve` function of the contract. + +```typescript +logger.log("Step 3: Retrieving the stored value..."); + +const retrieveResult = await paladin.call({ + type: TransactionType.PUBLIC, // Public call + abi: storageJson.abi, // ABI of the Storage contract + function: "retrieve", // Name of the function to call + from: owner.lookup, // Account making the call + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: {}, // No arguments required for this function +}); + +const retrievedValue = retrieveResult["0"]; +if (retrievedValue !== valueToStore.toString()) { + logger.error(`Retrieved value "${retrievedValue}" does not match stored value "${valueToStore}"!`); + return false; +} +logger.log(`Step 3: Value retrieved successfully! Retrieved value: "${retrievedValue}"`); +``` + +- **What happens**: + - The `call` function retrieves the stored value by interacting with the `retrieve` function of the contract. + - The retrieved value is validated against the previously stored value to ensure correctness. + +--- + +## Conclusion + +Congratulations! You’ve successfully: +1. Deployed the `Storage` contract, +2. Stored a value in the contract, and +3. Retrieved the stored value. + +--- + +## Next Steps + +Now that you've mastered deploying and interacting with a **public storage contract**, it's time to take things to the next level. In the next tutorial, you'll learn about **Storage with Privacy**, where you will add a privacy layer to the blockchain.the blockchain! + +[Continue to the Privacy Storage Contract Tutorial →](./private-storage.md) \ No newline at end of file diff --git a/doc-site/docs/tutorials/zkp-cbdc.md b/doc-site/docs/tutorials/zkp-cbdc.md index 2ed158338..538b3b39c 100644 --- a/doc-site/docs/tutorials/zkp-cbdc.md +++ b/doc-site/docs/tutorials/zkp-cbdc.md @@ -6,7 +6,7 @@ This shows how to leverage the [Zeto](../../architecture/zeto/) in order to buil ## Running the example -Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paldin environment, and +Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paladin environment, and then follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/zeto/README.md) to run the code. @@ -145,3 +145,9 @@ const result5 = await zetoCBDC.using(paladin1).withdraw(bank1, { ``` A participant like `bank1` who has unspent Zeto tokens can call `withdraw` on the Paladin Zeto domain to exchange them for ERC20 balances. Behind the scenes, the requested amount are "burnt" in the Zeto contract, and the corresponding ERC20 amount are released by the Zeto contract, by transferring to the requesting account. + +## Next Steps + +Next, discover how **Notarized Tokens** and **Privacy Groups** seamlessly integrate to enable a robust bond issuance workflow that balances private collaboration with public transparency. + +[Continue to the Bond Issuance Tutorial →](./bond-issuance.md) diff --git a/doc-site/mkdocs.yml b/doc-site/mkdocs.yml index 1ab9c05f6..43f3319fc 100644 --- a/doc-site/mkdocs.yml +++ b/doc-site/mkdocs.yml @@ -116,6 +116,9 @@ nav: - User Interface: getting-started/user-interface.md - Tutorials: - Introduction: tutorials/index.md + - Hello World: tutorials/hello-world.md + - Public Storage: tutorials/public-storage.md + - Private Storage: tutorials/private-storage.md - Bond Issuance: tutorials/bond-issuance.md - Wholesale CBDC: tutorials/zkp-cbdc.md - Reference: diff --git a/example/bond/src/index.ts b/example/bond/src/index.ts index da0b51032..a597c7106 100644 --- a/example/bond/src/index.ts +++ b/example/bond/src/index.ts @@ -1,12 +1,10 @@ import PaladinClient, { - Algorithms, encodeStates, newGroupSalt, newTransactionId, NotoFactory, PenteFactory, TransactionType, - Verifiers, } from "@lfdecentralizedtrust-labs/paladin-sdk"; import bondTrackerPublicJson from "./abis/BondTrackerPublic.json"; import atomFactoryJson from "./abis/AtomFactory.json"; diff --git a/example/helloworld/.gitignore b/example/helloworld/.gitignore new file mode 100644 index 000000000..4c79bef56 --- /dev/null +++ b/example/helloworld/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +src/abis/*.json \ No newline at end of file diff --git a/example/helloworld/.vscode/launch.json b/example/helloworld/.vscode/launch.json new file mode 100644 index 000000000..1f1326095 --- /dev/null +++ b/example/helloworld/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "runtimeExecutable": "npm", + "args": ["run", "start"], + "request": "launch", + "type": "node", + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/example/helloworld/README.md b/example/helloworld/README.md new file mode 100644 index 000000000..b2dac86d8 --- /dev/null +++ b/example/helloworld/README.md @@ -0,0 +1,43 @@ +# Example: Hello World + +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/hello-world/) for a detailed explanation. + +## Pre-requisites + +Requires a local Paladin instance running on `localhost:31548`. + +## Run standalone + +Compile [Solidity contracts](../../solidity): + +```shell +cd ../../solidity +npm install +npm run compile +``` + +Build [TypeScript SDK](../../sdk/typescript): + +```shell +cd ../../sdk/typescript +npm install +npm run abi +npm run build +``` + +Run example: + +```shell +npm install +npm run abi +npm run start +``` + +## Run with Gradle + +The following will perform all pre-requisites and then run the example: + +```shell +../../gradlew build +npm run start +``` diff --git a/example/helloworld/build.gradle b/example/helloworld/build.gradle new file mode 100644 index 000000000..59e2e8f00 --- /dev/null +++ b/example/helloworld/build.gradle @@ -0,0 +1,73 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +configurations { + // Resolvable configurations + contractCompile { + canBeConsumed = false + canBeResolved = true + } + buildSDK { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + contractCompile project(path: ':solidity', configuration: 'compiledContracts') + buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') +} + +task install(type: Exec) { + executable 'npm' + args 'install' + + inputs.files(configurations.buildSDK) + inputs.files('package.json') + outputs.files('package-lock.json') + outputs.dir('node_modules') +} + +task copyABI(type: Exec, dependsOn: install) { + executable 'npm' + args 'run' + args 'abi' + + inputs.files(configurations.contractCompile) + inputs.dir('scripts') + outputs.dir('src/abis') +} + +task build(type: Exec, dependsOn: [install, copyABI]) { + executable 'npm' + args 'run' + args 'build' + + inputs.dir('src') + outputs.dir('build') +} + +task e2e(type: Exec, dependsOn: [build]) { + dependsOn ':operator:deploy' + + executable 'npm' + args 'run' + args 'start' +} + +task clean(type: Delete) { + delete 'node_modules' + delete 'build' +} diff --git a/example/helloworld/package-lock.json b/example/helloworld/package-lock.json new file mode 100644 index 000000000..482bb2933 --- /dev/null +++ b/example/helloworld/package-lock.json @@ -0,0 +1,312 @@ +{ + "name": "paladin-example-helloworld", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paladin-example-helloworld", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + }, + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "../../sdk/typescript": { + "name": "@lfdecentralizedtrust-labs/paladin-sdk", + "version": "0.0.6-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.7", + "ethers": "^6.13.4", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "copy-file": "^11.0.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lfdecentralizedtrust-labs/paladin-sdk": { + "resolved": "../../sdk/typescript", + "link": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-file": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", + "integrity": "sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/example/helloworld/package.json b/example/helloworld/package.json new file mode 100644 index 000000000..55e03796c --- /dev/null +++ b/example/helloworld/package.json @@ -0,0 +1,23 @@ +{ + "name": "paladin-example-helloworld", + "version": "0.0.1", + "description": "", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "ts-node ./src/index.ts", + "start:prod": "node ./build/index.js", + "abi": "node scripts/abi.mjs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + } +} diff --git a/example/helloworld/scripts/abi.mjs b/example/helloworld/scripts/abi.mjs new file mode 100644 index 000000000..ec1aa1959 --- /dev/null +++ b/example/helloworld/scripts/abi.mjs @@ -0,0 +1,6 @@ +import { copyFile } from "copy-file"; + +await copyFile( + "../../solidity/artifacts/contracts/tutorials/HelloWorld.sol/HelloWorld.json", + "src/abis/HelloWorld.json" +); \ No newline at end of file diff --git a/example/helloworld/src/index.ts b/example/helloworld/src/index.ts new file mode 100644 index 000000000..d6b56174a --- /dev/null +++ b/example/helloworld/src/index.ts @@ -0,0 +1,97 @@ +import PaladinClient, { + TransactionType, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import helloWorldJson from "./abis/HelloWorld.json"; + +const logger = console; + +// Instantiate Paladin client (e.g., connecting to "node1") +const paladin = new PaladinClient({ + url: "http://127.0.0.1:31548", +}); + +async function main(): Promise { + // Retrieve the verifier for the owner account + const [owner] = paladin.getVerifiers("owner@node1"); + + // STEP 1: Deploy the HelloWorld contract + logger.log("STEP 1: Deploying the HelloWorld contract..."); + const deploymentTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Deploy publicly + abi: helloWorldJson.abi, // ABI of the HelloWorld contract + bytecode: helloWorldJson.bytecode, // Compiled bytecode + function: "", // No constructor arguments in this example + from: owner.lookup, // Account signing and endorsing the transaction + data: {}, // No additional data + }); + + // Wait for the deployment receipt + const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000, true); + if (!deploymentReceipt?.contractAddress) { + logger.error("STEP 1: Deployment failed!"); + return false; + } + logger.log("STEP 1: HelloWorld contract deployed successfully!"); + + // STEP 2: Call the sayHello function + logger.log("STEP 2: Calling the sayHello function..."); + const name = "Blocky McChainface"; // Example name for the greeting + + const sayHelloTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public transaction + abi: helloWorldJson.abi, // ABI of the HelloWorld contract + function: "sayHello", // Name of the function to call + from: owner.lookup, // Account signing and endorsing the transaction + to: deploymentReceipt.contractAddress, // Deployed contract address + data: { // Function arguments + name: name, // Name of the person to greet + }, + }); + + if (!sayHelloTxID) { + logger.error("STEP 2: Function call failed!"); + return false; + } + + // Wait for the function call receipt + const functionReceipt = await paladin.pollForReceipt(sayHelloTxID, 10000, true); + if (!functionReceipt?.transactionHash) { + logger.error("STEP 2: Receipt retrieval failed!"); + return false; + } + logger.log("STEP 2: sayHello function executed successfully!"); + + // STEP 3: Retrieve and verify the emitted event + logger.log("STEP 3: Retrieving and verifying emitted events..."); + const events = await paladin.decodeTransactionEvents( + functionReceipt.transactionHash, // Transaction hash + helloWorldJson.abi, // ABI of the contract + "pretty=true", // encoding format + ); + + // Extract the event message and validate its content + const message = events[0].data["message"]; + const expectedOutput = `Welcome to Paladin, ${name}:)`; + if (message !== expectedOutput) { + logger.error("STEP 3: Event data does not match the expected output!"); + return false; + } + logger.log("STEP 3: Events verified successfully!"); + + // Log the final message to the console + logger.log("\n", message, "\n"); + + return true; +} + +if (require.main === module) { + main() + .then((success: boolean) => { + process.exit(success ? 0 : 1); + }) + .catch((err) => { + console.error("Exiting with uncaught error"); + console.error(err); + process.exit(1); + }); +} diff --git a/example/helloworld/tsconfig.json b/example/helloworld/tsconfig.json new file mode 100644 index 000000000..f0fd3378b --- /dev/null +++ b/example/helloworld/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/example/notarized-tokens/.gitignore b/example/notarized-tokens/.gitignore new file mode 100644 index 000000000..4c79bef56 --- /dev/null +++ b/example/notarized-tokens/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +src/abis/*.json \ No newline at end of file diff --git a/example/notarized-tokens/.vscode/launch.json b/example/notarized-tokens/.vscode/launch.json new file mode 100644 index 000000000..1f1326095 --- /dev/null +++ b/example/notarized-tokens/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "runtimeExecutable": "npm", + "args": ["run", "start"], + "request": "launch", + "type": "node", + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/example/notarized-tokens/README.md b/example/notarized-tokens/README.md new file mode 100644 index 000000000..2a8e3ecca --- /dev/null +++ b/example/notarized-tokens/README.md @@ -0,0 +1,36 @@ +# Example: Notarized-Tokens + +This tutorial is meant to get familiar with how to create and manage a basic token using the **Notarized Tokens (noto)** domain. + +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/notarized-tokens/) for a detailed explanation. + +## Pre-requisites + +Requires a local 3-node Paladin cluster running on `localhost:31548`, `localhost:31648`, and `localhost:31748`. + +## Run standalone + +Build [TypeScript SDK](../../sdk/typescript): + +```shell +cd ../../sdk/typescript +npm install +npm run abi +npm run build +``` + +Run example: + +```shell +npm install +npm run start +``` + +## Run with Gradle + +The following will perform all pre-requisites and then run the example: + +```shell +../../gradlew build +npm run start +``` diff --git a/example/notarized-tokens/build.gradle b/example/notarized-tokens/build.gradle new file mode 100644 index 000000000..f38d57963 --- /dev/null +++ b/example/notarized-tokens/build.gradle @@ -0,0 +1,62 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +configurations { + // Resolvable configurations + contractCompile { + canBeConsumed = false + canBeResolved = true + } + buildSDK { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') +} + +task install(type: Exec) { + executable 'npm' + args 'install' + + inputs.files(configurations.buildSDK) + inputs.files('package.json') + outputs.files('package-lock.json') + outputs.dir('node_modules') +} + +task build(type: Exec, dependsOn: install) { + executable 'npm' + args 'run' + args 'build' + + inputs.dir('src') + outputs.dir('build') +} + +task e2e(type: Exec, dependsOn: [build]) { + dependsOn ':operator:deploy' + + executable 'npm' + args 'run' + args 'start' +} + +task clean(type: Delete) { + delete 'node_modules' + delete 'build' +} diff --git a/example/notarized-tokens/package-lock.json b/example/notarized-tokens/package-lock.json new file mode 100644 index 000000000..e657e182b --- /dev/null +++ b/example/notarized-tokens/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "paladin-example-noto", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paladin-example-noto", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + }, + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "../../sdk/typescript": { + "name": "@lfdecentralizedtrust-labs/paladin-sdk", + "version": "0.0.6-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.7", + "ethers": "^6.13.4", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "copy-file": "^11.0.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lfdecentralizedtrust-labs/paladin-sdk": { + "resolved": "../../sdk/typescript", + "link": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/copy-file": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", + "integrity": "sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.3.tgz", + "integrity": "sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/example/notarized-tokens/package.json b/example/notarized-tokens/package.json new file mode 100644 index 000000000..b273522b8 --- /dev/null +++ b/example/notarized-tokens/package.json @@ -0,0 +1,23 @@ +{ + "name": "paladin-example-noto", + "version": "0.0.1", + "description": "", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "ts-node ./src/index.ts", + "start:prod": "node ./build/index.js", + "abi": "node scripts/abi.mjs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + } +} diff --git a/example/notarized-tokens/src/index.ts b/example/notarized-tokens/src/index.ts new file mode 100644 index 000000000..7c429a419 --- /dev/null +++ b/example/notarized-tokens/src/index.ts @@ -0,0 +1,85 @@ +import PaladinClient, { + NotoFactory, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; + +const logger = console; + +// Initialize Paladin clients for three nodes +const paladinClientNode1 = new PaladinClient({ url: "http://127.0.0.1:31548" }); +const paladinClientNode2 = new PaladinClient({ url: "http://127.0.0.1:31648" }); +const paladinClientNode3 = new PaladinClient({ url: "http://127.0.0.1:31748" }); + +async function main(): Promise { + // Retrieve verifiers for each node + const [verifierNode1] = paladinClientNode1.getVerifiers("user@node1"); + const [verifierNode2] = paladinClientNode2.getVerifiers("user@node2"); + const [verifierNode3] = paladinClientNode3.getVerifiers("user@node3"); + + // Step 1: Deploy a Noto token to represent cash + logger.log("Step 1: Deploying a Noto cash token..."); + const notoFactory = new NotoFactory(paladinClientNode1, "noto"); + const cashToken = await notoFactory.newNoto(verifierNode1, { + notary: verifierNode1, // The notary for this token + restrictMinting: true, // Restrict minting to the notary only + }); + if (!cashToken) { + logger.error("Failed to deploy the Noto cash token!"); + return false; + } + logger.log("Noto cash token deployed successfully!"); + + // Step 2: Mint cash tokens + logger.log("Step 2: Minting 2000 units of cash to Node1..."); + const mintReceipt = await cashToken.mint(verifierNode1, { + to: verifierNode1, // Mint cash to Node1 + amount: 2000, // Amount to mint + data: "0x", // Optional additional data + }); + if (!mintReceipt) { + logger.error("Failed to mint cash tokens!"); + return false; + } + logger.log("Successfully minted 2000 units of cash to Node1!"); + + // Step 3: Transfer cash to Node2 + logger.log("Step 3: Transferring 1000 units of cash from Node1 to Node2..."); + const transferToNode2 = await cashToken.transfer(verifierNode1, { + to: verifierNode2, // Transfer to Node2 + amount: 1000, // Amount to transfer + data: "0x", // Optional additional data + }); + if (!transferToNode2) { + logger.error("Failed to transfer cash to Node2!"); + return false; + } + logger.log("Successfully transferred 1000 units of cash to Node2!"); + + // Step 4: Transfer cash to Node3 from Node2 + logger.log("Step 4: Transferring 800 units of cash from Node2 to Node3..."); + const transferToNode3 = await cashToken.using(paladinClientNode2).transfer(verifierNode2, { + to: verifierNode3, // Transfer to Node3 + amount: 800, // Amount to transfer + data: "0x", // Optional additional data + }); + if (!transferToNode3) { + logger.error("Failed to transfer cash to Node3!"); + return false; + } + logger.log("Successfully transferred 800 units of cash to Node3!"); + + // All steps completed successfully + logger.log("All operations completed successfully!"); + return true; +} + +// Execute the main function if this file is run directly +if (require.main === module) { + main() + .then((success: boolean) => { + process.exit(success ? 0 : 1); // Exit with 0 for success, 1 for failure + }) + .catch((err) => { + logger.error("Exiting due to an uncaught error:", err); + process.exit(1); // Exit with status 1 for any uncaught errors + }); +} diff --git a/example/notarized-tokens/tsconfig.json b/example/notarized-tokens/tsconfig.json new file mode 100644 index 000000000..854d27e37 --- /dev/null +++ b/example/notarized-tokens/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "./build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/example/privacy-storage/.gitignore b/example/privacy-storage/.gitignore new file mode 100644 index 000000000..4c79bef56 --- /dev/null +++ b/example/privacy-storage/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +src/abis/*.json \ No newline at end of file diff --git a/example/privacy-storage/.vscode/launch.json b/example/privacy-storage/.vscode/launch.json new file mode 100644 index 000000000..1f1326095 --- /dev/null +++ b/example/privacy-storage/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "runtimeExecutable": "npm", + "args": ["run", "start"], + "request": "launch", + "type": "node", + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/example/privacy-storage/README.md b/example/privacy-storage/README.md new file mode 100644 index 000000000..380ebeb7a --- /dev/null +++ b/example/privacy-storage/README.md @@ -0,0 +1,46 @@ +# Example: Storage with Privacy Groups + + +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/private-storage/) for a detailed explanation. + +## Pre-requisites + +Requires a local Paladin instance running on `localhost:31548`. +Requires a local Paladin instance running on `localhost:31648`. +Requires a local Paladin instance running on `localhost:31748`. + +## Run standalone + +Compile [Solidity contracts](../../solidity): + +```shell +cd ../../solidity +npm install +npm run compile +``` + +Build [TypeScript SDK](../../sdk/typescript): + +```shell +cd ../../sdk/typescript +npm install +npm run abi +npm run build +``` + +Run example: + +```shell +npm install +npm run abi +npm run start +``` + +## Run with Gradle + +The following will perform all pre-requisites and then run the example: + +```shell +../../gradlew build +npm run start +``` diff --git a/example/privacy-storage/build.gradle b/example/privacy-storage/build.gradle new file mode 100644 index 000000000..59e2e8f00 --- /dev/null +++ b/example/privacy-storage/build.gradle @@ -0,0 +1,73 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +configurations { + // Resolvable configurations + contractCompile { + canBeConsumed = false + canBeResolved = true + } + buildSDK { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + contractCompile project(path: ':solidity', configuration: 'compiledContracts') + buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') +} + +task install(type: Exec) { + executable 'npm' + args 'install' + + inputs.files(configurations.buildSDK) + inputs.files('package.json') + outputs.files('package-lock.json') + outputs.dir('node_modules') +} + +task copyABI(type: Exec, dependsOn: install) { + executable 'npm' + args 'run' + args 'abi' + + inputs.files(configurations.contractCompile) + inputs.dir('scripts') + outputs.dir('src/abis') +} + +task build(type: Exec, dependsOn: [install, copyABI]) { + executable 'npm' + args 'run' + args 'build' + + inputs.dir('src') + outputs.dir('build') +} + +task e2e(type: Exec, dependsOn: [build]) { + dependsOn ':operator:deploy' + + executable 'npm' + args 'run' + args 'start' +} + +task clean(type: Delete) { + delete 'node_modules' + delete 'build' +} diff --git a/example/privacy-storage/package-lock.json b/example/privacy-storage/package-lock.json new file mode 100644 index 000000000..75bb2c1e5 --- /dev/null +++ b/example/privacy-storage/package-lock.json @@ -0,0 +1,312 @@ +{ + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + }, + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "../../sdk/typescript": { + "name": "@lfdecentralizedtrust-labs/paladin-sdk", + "version": "0.0.6-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.7", + "ethers": "^6.13.4", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "copy-file": "^11.0.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lfdecentralizedtrust-labs/paladin-sdk": { + "resolved": "../../sdk/typescript", + "link": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-file": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", + "integrity": "sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/example/privacy-storage/package.json b/example/privacy-storage/package.json new file mode 100644 index 000000000..e49390adc --- /dev/null +++ b/example/privacy-storage/package.json @@ -0,0 +1,23 @@ +{ + "name": "paladin-example-privacy-storage", + "version": "0.0.1", + "description": "", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "ts-node ./src/index.ts", + "start:prod": "node ./build/index.js", + "abi": "node scripts/abi.mjs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + } +} diff --git a/example/privacy-storage/scripts/abi.mjs b/example/privacy-storage/scripts/abi.mjs new file mode 100644 index 000000000..eea65b73d --- /dev/null +++ b/example/privacy-storage/scripts/abi.mjs @@ -0,0 +1,6 @@ +import { copyFile } from "copy-file"; + +await copyFile( + "../../solidity/artifacts/contracts/tutorials/Storage.sol/Storage.json", + "src/abis/Storage.json", +); \ No newline at end of file diff --git a/example/privacy-storage/src/helpers/storage.ts b/example/privacy-storage/src/helpers/storage.ts new file mode 100644 index 000000000..76fedc1b2 --- /dev/null +++ b/example/privacy-storage/src/helpers/storage.ts @@ -0,0 +1,36 @@ +import PaladinClient, { + PaladinVerifier, + PentePrivacyGroup, + PentePrivateContract, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import storage from "../abis/Storage.json"; + +// export interface AddStorageParams { +// addr: string; +// } + +export const newPrivateStorage = async ( + pente: PentePrivacyGroup, + from: PaladinVerifier, +) => { + const address = await pente.deploy( + storage.abi, + storage.bytecode, + from, + ); + return address ? new PrivateStorage(pente, address) : undefined; +}; + + +export class PrivateStorage extends PentePrivateContract<{}> { + constructor( + protected evm: PentePrivacyGroup, + public readonly address: string + ) { + super(evm, storage.abi, address); + } + + using(paladin: PaladinClient) { + return new PrivateStorage(this.evm.using(paladin), this.address); + } +} diff --git a/example/privacy-storage/src/index.ts b/example/privacy-storage/src/index.ts new file mode 100644 index 000000000..48c117235 --- /dev/null +++ b/example/privacy-storage/src/index.ts @@ -0,0 +1,95 @@ +import PaladinClient, { + PenteFactory, + newGroupSalt, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import storageJson from "./abis/Storage.json"; +import { checkDeploy } from "./util"; +import { PrivateStorage } from "./helpers/storage"; + +const logger = console; + +// Initialize Paladin clients for three nodes +const paladinNode1 = new PaladinClient({ url: "http://127.0.0.1:31548" }); +const paladinNode2 = new PaladinClient({ url: "http://127.0.0.1:31648" }); +const paladinNode3 = new PaladinClient({ url: "http://127.0.0.1:31748" }); + +async function main(): Promise { + // Get verifiers for each node + const [verifierNode1] = paladinNode1.getVerifiers("member@node1"); + const [verifierNode2] = paladinNode2.getVerifiers("member@node2"); + const [verifierNode3] = paladinNode3.getVerifiers("outsider@node3"); + + // Step 1: Create a privacy group for members + logger.log("Creating a privacy group for Node1 and Node2..."); + const penteFactory = new PenteFactory(paladinNode1, "pente"); + const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { + group: { + salt: newGroupSalt(), // Generate a new salt for the group + members: [verifierNode1, verifierNode2], // Add members to the group + }, + evmVersion: "shanghai", + endorsementType: "group_scoped_identities", + externalCallsEnabled: true, + }); + + if (!checkDeploy(memberPrivacyGroup)) return false; + + // Step 2: Deploy a smart contract within the privacy group + logger.log("Deploying a smart contract to the privacy group..."); + const contractAddress = await memberPrivacyGroup.deploy( + storageJson.abi, // ABI of the contract + storageJson.bytecode, // Bytecode of the contract + verifierNode1 // Deploying as Node1 + ); + + if (!contractAddress) { + logger.error("Failed to deploy the contract. No address returned."); + return false; + } + + logger.log(`Contract deployed successfully! Address: ${contractAddress}`); + + // Step 3: Use the deployed contract for private storage + const privateStorageContract = new PrivateStorage(memberPrivacyGroup, contractAddress); + + // Store a value in the contract + logger.log("Storing a value (125) in the contract..."); + const storeTx = await privateStorageContract.invoke(verifierNode1, "store", { num: 125 }); + logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); + + // Retrieve the value as Node1 + logger.log("Node1 retrieving the value from the contract..."); + const retrievedValueNode1 = await privateStorageContract.call(verifierNode1, "retrieve", []); + logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); + + // Retrieve the value as Node2 + logger.log("Node2 retrieving the value from the contract..."); + const retrievedValueNode2 = await privateStorageContract + .using(paladinNode2) + .call(verifierNode2, "retrieve", []); + logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); + + // Attempt to retrieve the value as Node3 (outsider) + try { + logger.log("Node3 (outsider) attempting to retrieve the value..."); + await privateStorageContract.using(paladinNode3).call(verifierNode3, "retrieve", []); + logger.error("Node3 (outsider) should not have access to the privacy group!"); + return false; + } catch (error) { + logger.info("Node3 (outsider) cannot retrieve the data from the privacy group. Access denied."); + } + + return true; +} + +// Execute the main function when this file is run directly +if (require.main === module) { + main() + .then((success: boolean) => { + process.exit(success ? 0 : 1); // Exit with status 0 for success, 1 for failure + }) + .catch((err) => { + logger.error("Exiting due to an uncaught error:", err); + process.exit(1); // Exit with status 1 for any uncaught errors + }); +} \ No newline at end of file diff --git a/example/privacy-storage/src/util.ts b/example/privacy-storage/src/util.ts new file mode 100644 index 000000000..1379fed65 --- /dev/null +++ b/example/privacy-storage/src/util.ts @@ -0,0 +1,32 @@ +import { ITransactionReceipt } from "@lfdecentralizedtrust-labs/paladin-sdk"; + +const logger = console; + +export interface DeployedContract { + address: string; +} + +export function checkDeploy( + contract: DeployedContract | undefined +): contract is DeployedContract { + if (contract === undefined) { + logger.error("Failed!"); + return false; + } + logger.log(`Success! address: ${contract.address}`); + return true; +} + +export function checkReceipt( + receipt: ITransactionReceipt | undefined +): receipt is ITransactionReceipt { + if (receipt === undefined) { + logger.error("Failed!"); + return false; + } else if (receipt.failureMessage !== undefined) { + logger.error(`Failed: ${receipt.failureMessage}`); + return false; + } + logger.log("Success!"); + return true; +} diff --git a/example/privacy-storage/tsconfig.json b/example/privacy-storage/tsconfig.json new file mode 100644 index 000000000..f0fd3378b --- /dev/null +++ b/example/privacy-storage/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/example/public-storage/.gitignore b/example/public-storage/.gitignore new file mode 100644 index 000000000..4c79bef56 --- /dev/null +++ b/example/public-storage/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +src/abis/*.json \ No newline at end of file diff --git a/example/public-storage/.vscode/launch.json b/example/public-storage/.vscode/launch.json new file mode 100644 index 000000000..1f1326095 --- /dev/null +++ b/example/public-storage/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "runtimeExecutable": "npm", + "args": ["run", "start"], + "request": "launch", + "type": "node", + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/example/public-storage/README.md b/example/public-storage/README.md new file mode 100644 index 000000000..a0fe0f9bb --- /dev/null +++ b/example/public-storage/README.md @@ -0,0 +1,42 @@ +# Example: Public storage + +See the [tutorial](https://lf-decentralized-trust-labs.github.io/paladin/head/tutorials/public-storage/) for a detailed explanation. + +## Pre-requisites + +Requires a local Paladin instance running on `localhost:31548`. +## Run standalone + +Compile [Solidity contracts](../../solidity): + +```shell +cd ../../solidity +npm install +npm run compile +``` + +Build [TypeScript SDK](../../sdk/typescript): + +```shell +cd ../../sdk/typescript +npm install +npm run abi +npm run build +``` + +Run example: + +```shell +npm install +npm run abi +npm run start +``` + +## Run with Gradle + +The following will perform all pre-requisites and then run the example: + +```shell +../../gradlew build +npm run start +``` diff --git a/example/public-storage/build.gradle b/example/public-storage/build.gradle new file mode 100644 index 000000000..59e2e8f00 --- /dev/null +++ b/example/public-storage/build.gradle @@ -0,0 +1,73 @@ +/* + * Copyright © 2024 Kaleido, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +configurations { + // Resolvable configurations + contractCompile { + canBeConsumed = false + canBeResolved = true + } + buildSDK { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + contractCompile project(path: ':solidity', configuration: 'compiledContracts') + buildSDK project(path: ':sdk:typescript', configuration: 'buildSDK') +} + +task install(type: Exec) { + executable 'npm' + args 'install' + + inputs.files(configurations.buildSDK) + inputs.files('package.json') + outputs.files('package-lock.json') + outputs.dir('node_modules') +} + +task copyABI(type: Exec, dependsOn: install) { + executable 'npm' + args 'run' + args 'abi' + + inputs.files(configurations.contractCompile) + inputs.dir('scripts') + outputs.dir('src/abis') +} + +task build(type: Exec, dependsOn: [install, copyABI]) { + executable 'npm' + args 'run' + args 'build' + + inputs.dir('src') + outputs.dir('build') +} + +task e2e(type: Exec, dependsOn: [build]) { + dependsOn ':operator:deploy' + + executable 'npm' + args 'run' + args 'start' +} + +task clean(type: Delete) { + delete 'node_modules' + delete 'build' +} diff --git a/example/public-storage/package-lock.json b/example/public-storage/package-lock.json new file mode 100644 index 000000000..6e726c967 --- /dev/null +++ b/example/public-storage/package-lock.json @@ -0,0 +1,312 @@ +{ + "name": "paladin-example-public-storage", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paladin-example-public-storage", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + }, + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + } + }, + "../../sdk/typescript": { + "name": "@lfdecentralizedtrust-labs/paladin-sdk", + "version": "0.0.6-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.7.7", + "ethers": "^6.13.4", + "uuid": "^11.0.2" + }, + "devDependencies": { + "@types/node": "^22.9.0", + "copy-file": "^11.0.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lfdecentralizedtrust-labs/paladin-sdk": { + "resolved": "../../sdk/typescript", + "link": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-file": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-file/-/copy-file-11.0.0.tgz", + "integrity": "sha512-mFsNh/DIANLqFt5VHZoGirdg7bK5+oTWlhnGu6tgRhzBlnEKWaPX2xrFaLltii/6rmhqFMJqffUgknuRdpYlHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.11", + "p-event": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/example/public-storage/package.json b/example/public-storage/package.json new file mode 100644 index 000000000..9550776d8 --- /dev/null +++ b/example/public-storage/package.json @@ -0,0 +1,23 @@ +{ + "name": "paladin-example-public-storage", + "version": "0.0.1", + "description": "", + "main": "build/index.js", + "scripts": { + "build": "tsc", + "start": "ts-node ./src/index.ts", + "start:prod": "node ./build/index.js", + "abi": "node scripts/abi.mjs" + }, + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "^22.8.7", + "copy-file": "^11.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@lfdecentralizedtrust-labs/paladin-sdk": "file:../../sdk/typescript" + } +} diff --git a/example/public-storage/scripts/abi.mjs b/example/public-storage/scripts/abi.mjs new file mode 100644 index 000000000..2da0b9f18 --- /dev/null +++ b/example/public-storage/scripts/abi.mjs @@ -0,0 +1,6 @@ +import { copyFile } from "copy-file"; + +await copyFile( + "../../solidity/artifacts/contracts/tutorials/Storage.sol/Storage.json", + "src/abis/Storage.json" +); \ No newline at end of file diff --git a/example/public-storage/src/index.ts b/example/public-storage/src/index.ts new file mode 100644 index 000000000..e176fc5b7 --- /dev/null +++ b/example/public-storage/src/index.ts @@ -0,0 +1,90 @@ +import PaladinClient, { + TransactionType, +} from "@lfdecentralizedtrust-labs/paladin-sdk"; +import storageJson from "./abis/Storage.json"; + +const logger = console; + +// Instantiate Paladin client +const paladin = new PaladinClient({ + url: "http://127.0.0.1:31548", +}); + +async function main(): Promise { + // Get the owner account verifier + const [owner] = paladin.getVerifiers("owner@node1"); + + // Step 1: Deploy the Storage contract + logger.log("Step 1: Deploying the Storage contract..."); + const deploymentTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public deployment + abi: storageJson.abi, // ABI of the Storage contract + bytecode: storageJson.bytecode, // Compiled bytecode + function: "", // No constructor arguments + from: owner.lookup, // Account signing the transaction + data: {}, // No additional data + }); + + // Wait for deployment receipt + const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000); + if (!deploymentReceipt?.contractAddress) { + logger.error("Deployment failed!"); + return false; + } + logger.log("Step 1: Storage contract deployed successfully!"); + + // Step 3: Store a value in the contract + const valueToStore = 125; // Example value to store + logger.log(`Step 2: Storing value "${valueToStore}" in the contract...`); + const storeTxID = await paladin.sendTransaction({ + type: TransactionType.PUBLIC, // Public transaction + abi: storageJson.abi, // ABI of the Storage contract + function: "store", // Name of the function to call + from: owner.lookup, // Account signing the transaction + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: { num: valueToStore }, // Function arguments + }); + + // Wait for the store transaction receipt + const storeReceipt = await paladin.pollForReceipt(storeTxID, 10000); + if (!storeReceipt?.transactionHash) { + logger.error("Failed to store value in the contract!"); + return false; + } + logger.log("Step 2: Value stored successfully!" ); + + // Step 4: Retrieve the stored value from the contract + logger.log("Step 3: Retrieving the stored value..."); + const retrieveResult = await paladin.call({ + type: TransactionType.PUBLIC, // Public call + abi: storageJson.abi, // ABI of the Storage contract + function: "retrieve", // Name of the function to call + from: owner.lookup, // Account making the call + to: deploymentReceipt.contractAddress, // Address of the deployed contract + data: {}, // No arguments required for this function + }); + + // Validate the retrieved value + const retrievedValue = retrieveResult["0"]; + if (retrievedValue !== valueToStore.toString()) { + logger.error(`Retrieved value "${retrievedValue}" does not match stored value "${valueToStore}"!`); + return false; + } + + logger.log(`Step 3: Value retrieved successfully! Retrieved value: "${retrievedValue}"`); + + return true; +} + +// Entry point +if (require.main === module) { + main() + .then((success: boolean) => { + process.exit(success ? 0 : 1); + }) + .catch((err) => { + logger.error("Exiting with uncaught error"); + logger.error(err); + process.exit(1); + }); +} diff --git a/example/public-storage/tsconfig.json b/example/public-storage/tsconfig.json new file mode 100644 index 000000000..f0fd3378b --- /dev/null +++ b/example/public-storage/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "outDir": "build", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": false, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} diff --git a/sdk/typescript/src/domains/pente.ts b/sdk/typescript/src/domains/pente.ts index e3011b385..418519631 100644 --- a/sdk/typescript/src/domains/pente.ts +++ b/sdk/typescript/src/domains/pente.ts @@ -174,23 +174,34 @@ export class PentePrivacyGroup { ); } + // deploy a contract async deploy( abi: ReadonlyArray, bytecode: string, from: PaladinVerifier, - inputs: ConstructorParams + inputs?: ConstructorParams ) { + + // Find the constructor in the ABI const constructor = abi.find((entry) => entry.type === "constructor"); - if (constructor === undefined) { - throw new Error("Constructor not found"); - } + + // Handle the absence of a constructor + const constructorInputs = constructor?.inputs ?? []; + + // Prepare the data object + const data: Record = { + group: this.group, + bytecode, + inputs: inputs ?? [], // Ensure `inputs` is always included, defaulting to an empty array + }; + const txID = await this.paladin.sendTransaction({ type: TransactionType.PRIVATE, - abi: [privateDeployABI(constructor.inputs ?? [])], + abi: [privateDeployABI(constructorInputs ?? [])], function: "deploy", to: this.address, from: from.lookup, - data: { group: this.group, bytecode, inputs }, + data: data }); const receipt = await this.paladin.pollForReceipt( txID, @@ -200,6 +211,7 @@ export class PentePrivacyGroup { return receipt?.domainReceipt?.receipt.contractAddress; } + // invoke functions in the contract async invoke( from: PaladinVerifier, to: string, @@ -217,6 +229,7 @@ export class PentePrivacyGroup { return this.paladin.pollForReceipt(txID, this.options.pollTimeout); } + // call functions in the contract (read-only) async call( from: PaladinVerifier, to: string, diff --git a/settings.gradle b/settings.gradle index 9ed5fbcc9..e186f0912 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,6 +9,10 @@ include 'domains:pente' include 'domains:integration-test' include 'example:bond' include 'example:zeto' +include 'example:helloworld' +include 'example:public-storage' +include 'example:privacy-storage' +include 'example:notarized-tokens' include 'operator' include 'sdk:typescript' include 'transports:grpc' diff --git a/solidity/contracts/tutorials/HelloWorld.sol b/solidity/contracts/tutorials/HelloWorld.sol new file mode 100644 index 000000000..4030c1c14 --- /dev/null +++ b/solidity/contracts/tutorials/HelloWorld.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/** + * @title HelloWorld + * @dev A simple contract that emits an event with a welcome message + */ + +contract HelloWorld { + // Define an event + event HelloEvent(string message); + + /** + * @dev Emits an event with a welcome message for the given name + * @param name The name of the person + */ + function sayHello(string memory name) public { + // Format the message using string concatenation + string memory message = string(abi.encodePacked("Welcome to Paladin, ", name, ":)")); + emit HelloEvent(message); // Emit the event with the formatted message + } +} diff --git a/solidity/contracts/tutorials/Storage.sol b/solidity/contracts/tutorials/Storage.sol new file mode 100644 index 000000000..ed58ec194 --- /dev/null +++ b/solidity/contracts/tutorials/Storage.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @title Storage + * @dev Store & retrieve value in a variable + */ + +contract Storage { + + uint256 number; + + /** + * @dev Store value in variable + * @param num value to store + */ + function store(uint256 num) public { + number = num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} From f2a03aa6330b6dd808cf80c44c9bd6fbd01fd822 Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Fri, 7 Feb 2025 13:00:32 -0500 Subject: [PATCH 2/7] PR fixes Signed-off-by: David Wertenteil --- doc-site/docs/tutorials/hello-world.md | 51 ++++--- doc-site/docs/tutorials/notarized-tokens.md | 110 +++++++++----- doc-site/docs/tutorials/private-storage.md | 19 ++- doc-site/docs/tutorials/public-storage.md | 141 +++++++++++------- example/helloworld/src/index.ts | 31 ++-- example/notarized-tokens/src/index.ts | 22 +-- .../privacy-storage/src/helpers/storage.ts | 5 - example/privacy-storage/src/index.ts | 14 +- example/public-storage/src/index.ts | 37 +++-- sdk/typescript/src/interfaces/transaction.ts | 2 +- solidity/contracts/tutorials/HelloWorld.sol | 15 +- solidity/contracts/tutorials/Storage.sol | 20 ++- 12 files changed, 272 insertions(+), 195 deletions(-) diff --git a/doc-site/docs/tutorials/hello-world.md b/doc-site/docs/tutorials/hello-world.md index bcc1ce0ab..054202ef9 100644 --- a/doc-site/docs/tutorials/hello-world.md +++ b/doc-site/docs/tutorials/hello-world.md @@ -31,24 +31,34 @@ These are pre-compiled and provided in the `helloWorldJson` object. --- +To address the PR comment and clarify the differences between **contract deployment** and **function invocation**, here’s a revised version of the tutorial with an explicit callout: + +--- + ### Step 1: Deploy the Contract ```typescript const deploymentTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Deploy publicly - abi: helloWorldJson.abi, // ABI of the HelloWorld contract - bytecode: helloWorldJson.bytecode, // Compiled bytecode - function: "", // No constructor arguments - from: owner.lookup, // Account signing the transaction - data: {}, // No additional data + type: TransactionType.PUBLIC, + abi: helloWorldJson.abi, + bytecode: helloWorldJson.bytecode, + from: owner.lookup, + data: {}, }); ``` -- **What happens**: - - The `sendTransaction` method sends a deployment transaction to the Paladin network. - - The function returns a `deploymentTxID` that uniquely identifies the transaction. +#### Key Differences (vs. calling a contract function) +- **Deployment requires `bytecode`**, as it is creating a new contract on the blockchain. +- **No `to` address is specified**, since a contract does not yet exist at this stage. +- **No specific function is called**, since this is an initial deployment. -### Step 2: Confirm the Deployment +#### What happens: +- The `sendTransaction` method sends a deployment transaction to the blockchain via Paladin. +- The function returns a `deploymentTxID` that uniquely identifies the transaction. + +--- + +### Step 2: Confirm the Deployment ```typescript const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000, true); @@ -59,16 +69,16 @@ if (!deploymentReceipt?.contractAddress) { logger.log("Contract deployed successfully at address:", deploymentReceipt.contractAddress); ``` -- **What happens**: - - We use `pollForReceipt` to wait for the deployment transaction to be confirmed. - - If successful, the receipt includes the new `contractAddress`. +#### What happens: +- We use `pollForReceipt` to wait for the deployment transaction to be confirmed. +- If successful, the receipt includes the new `contractAddress`, which we will use in the next step. --- -### Step 3: Call the `sayHello` Function +### **Step 3: Call the `sayHello` Function** ```typescript -const name = "Blocky McChainface"; // Example name for the greeting +const name = "Paladin User"; // Example name for the greeting const sayHelloTxID = await paladin.sendTransaction({ type: TransactionType.PUBLIC, @@ -80,9 +90,14 @@ const sayHelloTxID = await paladin.sendTransaction({ }); ``` -- **What happens**: - - The `sendTransaction` method sends a transaction to call the `sayHello` function of the deployed contract. - - The `data` object includes the function arguments—in this case, the `name` of the person being greeted. +#### **Key Differences (vs. contract deployment)** +- **Function calls require a `to` address**, since the contract already exists. +- **No `bytecode` is needed**, as we are invoking an existing contract, not creating one. +- **A specific function (`sayHello`) is provided**, along with its arguments in `data`. + +#### **What happens:** +- The `sendTransaction` method sends a transaction to call the `sayHello` function of the deployed contract. +- The `data` object includes the function arguments—in this case, the `name` of the person being greeted. --- diff --git a/doc-site/docs/tutorials/notarized-tokens.md b/doc-site/docs/tutorials/notarized-tokens.md index 45cfa8519..40fb33f0d 100644 --- a/doc-site/docs/tutorials/notarized-tokens.md +++ b/doc-site/docs/tutorials/notarized-tokens.md @@ -1,62 +1,77 @@ # Notarized Tokens -In this tutorial, you’ll learn how to create and manage a basic token using the **Notarized Tokens (noto)** domain. Unlike the private storage example, these tokens can be transferred between nodes publicly, demonstrating how assets (e.g., “cash”) can be issued and tracked on the blockchain using Paladin. +In this tutorial, you’ll learn how to create and manage **Notarized Tokens (Noto)** within Paladin. Unlike simple private storage, **Notarized Tokens allow secure, private exchanges while maintaining verifiability**. + +## Why Use Notarized Tokens? + +- **Privacy-Preserving Transfers** – Transactions remain private, visible only to relevant parties. +- **Notary-Controlled Oversight** – A designated **notary** approves and submits every transaction, ensuring compliance and control. +- **Selective Disclosure** – Owners can prove token ownership by selectively revealing transaction details when needed. + +This tutorial will guide you through issuing, transferring, and verifying tokens using Paladin’s notarization model. + +--- ## Prerequisites -Make sure you have: +Before starting, ensure you have: 1. Completed the [Private Storage Tutorial](./private-storage.md). -2. A **running Paladin network** with at least three nodes (Node1, Node2, Node3). +2. A **running Paladin network** with at least three nodes (**Node1, Node2, and Node3**). --- ## Overview -This tutorial will guide you through: +This tutorial will cover: -1. **Deploying a Noto Token**: Use the `NotoFactory` to create a “cash token” with a specific notary. -2. **Minting Tokens**: Issue tokens to a particular node’s account. -3. **Transferring Tokens**: Send tokens between different nodes to simulate basic payments. +1. **Deploying a Noto Token** – Creating a “cash token” with a designated notary. +2. **Minting Tokens** – Issuing new tokens into circulation. +3. **Transferring Tokens** – Simulating payments by moving tokens between nodes. -You can find the complete example code in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/tree/main/example/notarized-tokens). +💡 **The complete example code is available in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/tree/main/example/notarized-tokens).** --- ## Step 1: Deploy a Noto Token -First, create a **Noto Factory** instance and deploy a new token. In this scenario, Node1 will act as both the notary (the entity allowed to mint tokens) and the initial recipient of the minted cash. +First, create a **Noto Factory** instance and deploy a new token. **Node1** will act as the **notary**, responsible for approving and submitting all transactions related to this token. Additionally, **Node1 will be the initial recipient of the minted tokens**. ```typescript logger.log("Step 1: Deploying a Noto cash token..."); const notoFactory = new NotoFactory(paladinClientNode1, "noto"); const cashToken = await notoFactory.newNoto(verifierNode1, { - notary: verifierNode1, // The notary for this token - restrictMinting: true, // Restrict minting to the notary only +notary: verifierNode1,// The notary overseeing ALL token transactions +notaryMode: "basic",// The notary mode }); if (!cashToken) { - logger.error("Failed to deploy the Noto cash token!"); - return false; +logger.error("Failed to deploy the Noto cash token!"); +return false; } logger.log("Noto cash token deployed successfully!"); ``` -**Key Points**: -- **`notary`**: Specifies which verifier (account) can mint new tokens. -- **`restrictMinting`**: If `true`, only the `notary` can mint additional tokens. +### Why the Notary Role Matters +The **notary** is more than just a minting authority—it plays a fundamental role in the **Noto token model**: + +- **Approves and submits all token transactions** to the network. +- **Maintains full visibility** over all token movements. +- **Ensures transaction integrity and compliance** with predefined rules. + +By designating a notary, every transaction must be verified and approved, ensuring controlled and auditable token transfers. --- ## Step 2: Mint Tokens -Now that the token contract exists, **mint** an initial supply of tokens to Node1. This step simulates creating new “cash” in the system. +With the token contract deployed, let’s **mint** an initial supply of tokens for Node1. This simulates creating new “cash” in the system. ```typescript logger.log("Step 2: Minting 2000 units of cash to Node1..."); const mintReceipt = await cashToken.mint(verifierNode1, { - to: verifierNode1, // Mint cash to Node1 - amount: 2000, // Amount to mint - data: "0x", // Additional data (optional) + to: verifierNode1, + amount: 2000, + data: "0x", }); if (!mintReceipt) { logger.error("Failed to mint cash tokens!"); @@ -65,22 +80,32 @@ if (!mintReceipt) { logger.log("Successfully minted 2000 units of cash to Node1!"); ``` -**Key Points**: -- **`amount`**: Number of tokens to create. -- **`data`**: Can include extra metadata or encoding, if needed. +### What Happens Here? + +1. **Node1 submits a minting request** to the notary. +2. **The notary reviews and approves** the request. +3. **Tokens are minted and assigned** to the recipient. +4. **The `data` field is recorded** in the transaction receipt for auditability. + +### Key Parameters +- **`amount`** – Number of tokens to create. +- **`to`** – Recipient of the newly minted tokens. +- **`data`** – (Optional) Can include metadata or extra information about the transaction. + +💡 **The data field is stored in the transaction receipt, making it useful for audits or tracking purposes.** --- ## Step 3: Transfer Tokens to Node2 -With tokens minted on Node1, you can **transfer** some of them to Node2. This step demonstrates a simple token transfer, much like sending money to another account. +Now that Node1 has tokens, let’s **transfer some to Node2**. This works similarly to a bank transfer. ```typescript logger.log("Step 3: Transferring 1000 units of cash from Node1 to Node2..."); const transferToNode2 = await cashToken.transfer(verifierNode1, { - to: verifierNode2, // Transfer to Node2 - amount: 1000, // Amount to transfer - data: "0x", // Optional additional data + to: verifierNode2, + amount: 1000, + data: "0x", }); if (!transferToNode2) { logger.error("Failed to transfer cash to Node2!"); @@ -93,14 +118,14 @@ logger.log("Successfully transferred 1000 units of cash to Node2!"); ## Step 4: Transfer Tokens to Node3 -Now let’s see how Node2 can pass tokens to Node3. This step involves calling `.using(paladinClientNode2)` so that **Node2** signs the transaction rather than Node1. +Now let’s see how **Node2** transfers tokens to **Node3**. Since Node2 is initiating the transaction, we call `.using(paladinClientNode2)` to ensure **Node2 signs the transaction instead of Node1**. ```typescript logger.log("Step 4: Transferring 800 units of cash from Node2 to Node3..."); const transferToNode3 = await cashToken.using(paladinClientNode2).transfer(verifierNode2, { - to: verifierNode3, // Transfer to Node3 - amount: 800, // Amount to transfer - data: "0x", // Optional additional data + to: verifierNode3, + amount: 800, + data: "0x", }); if (!transferToNode3) { logger.error("Failed to transfer cash to Node3!"); @@ -109,27 +134,32 @@ if (!transferToNode3) { logger.log("Successfully transferred 800 units of cash to Node3!"); ``` -**Key Points**: -- **`.using(paladinClientNode2)`** ensures the transaction is signed by Node2. -- If Node2 does not have sufficient tokens (e.g., tries to transfer 1200 while only having 1000), the transfer should fail and return an error. +### Transaction Privacy in Paladin + +Unlike traditional blockchains, **Paladin’s notarized token model ensures that not all participants see every transaction**: + +- **The notary has full visibility** over all token transfers. +- **Node2 and Node3 only see transactions they were involved in.** +- **Other nodes have no visibility into the transfer.** + --- ## Conclusion -Congratulations! You’ve successfully: +Congratulations! You have successfully: -1. **Deployed a Noto token** to represent cash within the Paladin network. -2. **Minted tokens** from a designated notary account. -3. **Transferred tokens** between different nodes, demonstrating how digital assets move across participants. +1. **Deployed a Noto token** to represent cash within the Paladin network. +2. **Minted tokens** under a notary’s supervision. +3. **Transferred tokens** between nodes while maintaining privacy and control. -At this point, you have a basic grasp of how to issue and manage tokens using the Noto domain. +At this point, you understand how to issue, manage, and transfer notarized tokens within Paladin. --- ## Next Steps -Now that you’ve explored how to create, mint, and transfer tokens using the Noto domain, you’re ready to delve into Zeto, Paladin’s zero-knowledge domain for more advanced privacy features. In the next tutorial, you’ll learn how to build a cash payment solution—for example, a wholesale CBDC or a commercial bank money rail—while leveraging powerful privacy techniques such as private minting and selective disclosure. +Now that you’ve explored **Notarized Tokens**, you’re ready to delve into **Zeto**, Paladin’s **zero-knowledge domain** for enhanced privacy. In the next tutorial, you’ll learn how to build **a privacy-preserving cash payment system** using advanced techniques such as **private minting and selective disclosure**. [Continue to the Zero-Knowledge Proof Tutorial →](./zkp-cbdc.md) diff --git a/doc-site/docs/tutorials/private-storage.md b/doc-site/docs/tutorials/private-storage.md index a39e1526f..33a4a1ecd 100644 --- a/doc-site/docs/tutorials/private-storage.md +++ b/doc-site/docs/tutorials/private-storage.md @@ -101,20 +101,24 @@ Authorized group members can retrieve the stored value. ```typescript logger.log("Node1 retrieving the stored value..."); const retrievedValueNode1 = await privateStorage.call(verifierNode1, "retrieve", []); -logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); +logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["value"]); logger.log("Node2 retrieving the stored value..."); const retrievedValueNode2 = await privateStorage .using(paladinNode2) .call(verifierNode2, "retrieve", []); -logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); +logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["value"]); ``` --- ## Step 4: Verify Privacy by Testing Unauthorized Access -When an outsider (Node3) tries to access the private contract, the attempt should fail. +In a **privacy group**, all **inputs and outputs of transactions remain private** among group members. This means that **Node3 (an outsider) cannot reconstruct the contract’s current state** because it was never included in the private state updates. + +Unlike traditional access control mechanisms where permissions are enforced at the contract level, **Paladin’s privacy groups ensure that only the designated members receive and share the necessary state information**. As a result, **Node3 does not have access to any past transactions or stored values, preventing it from reconstructing the contract state**. + +### Testing Unauthorized Access ```typescript try { @@ -123,11 +127,16 @@ try { logger.error("Node3 (outsider) should not have access to the private Storage contract!"); return false; } catch (error) { - logger.info("Node3 (outsider) cannot retrieve data. Access denied."); + logger.info("Node3 (outsider) cannot retrieve data because it was never included in the private state updates."); } ``` ---- +### Why Privacy Groups Work +- **Private State Isolation** – Transactions within a privacy group are only visible to its members. +- **No Global State Sharing** – Outsiders (e.g., Node3) never receive the transaction history, making it impossible for them to infer contract data. +- **Selective State Distribution** – Only group members can access and verify the shared state. + +By design, **Node3 does not just “lack permission” to call the contract—it lacks any knowledge of its state, history, or data, making unauthorized access fundamentally impossible**. ## Conclusion diff --git a/doc-site/docs/tutorials/public-storage.md b/doc-site/docs/tutorials/public-storage.md index f05b4165d..a1a250881 100644 --- a/doc-site/docs/tutorials/public-storage.md +++ b/doc-site/docs/tutorials/public-storage.md @@ -1,131 +1,162 @@ # Public Storage Contract -In the [previous tutorial](./hello-world.md), we deployed and interacted with a **HelloWorld** contract that emitted an event. Now, we will take it a step further by deploying a **Storage** contract that: -1. Allows you to store a value on the blockchain. -2. Lets you retrieve the stored value. +In the [previous tutorial](./hello-world.md), we deployed and interacted with a **HelloWorld** contract that emitted an event. Now, we will go a step further and deploy a **Storage** contract that: + +1. **Stores values** on the blockchain +2. **Retrieves stored values** on demand + +This tutorial will guide you through **deploying, storing, and retrieving data** using the Paladin SDK --- ## Prerequisites -- You’ve completed the [HelloWorld Tutorial](./hello-world.md) and are familiar with: - - Deploying contracts with the Paladin SDK. - - Sending transactions and retrieving their receipts. +Before you begin, ensure that you: + +- Completed the [HelloWorld Tutorial](./hello-world.md), where you learned: + - How to deploy contracts using the Paladin SDK + - How to send transactions and retrieve receipts +- Have access to a **Paladin network** to deploy and interact with smart contracts --- ## Overview -The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/public-storage). +The `Storage` contract provides two primary functions: + +- **`store(uint256 num)`** – Stores a value in the contract +- **`retrieve()`** – Retrieves the last stored value + +### Paladin API & Ethereum Similarities +Paladin’s API design follows **Ethereum JSON-RPC patterns**, making it familiar to developers who have used standard Ethereum APIs: +- **Transactions (`sendTransaction`)** → Similar to `eth_sendTransaction`, used for modifying on-chain state +- **Calls (`call`)** → Similar to `eth_call`, used for reading blockchain state without modifying it + +> 💡 **Numbers in Paladin are passed as strings** by default, consistent with JSON-RPC standards + +--- -The storage solidity contract can be found [here](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). +## Where to Find the Code? -The `Storage` contract provides two main functions: -1. **`store(uint256 num)`**: Stores a value in the contract. -2. **`retrieve()`**: Retrieves the last stored value. +🔹 Example implementation: [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/public-storage) +🔹 Solidity contract: [Storage.sol](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol) +--- -### Step 1: Deploy the Contract +## Step 1: Deploy the Contract -The first step is to deploy the `Storage` contract to the blockchain. +The first step is to deploy the `Storage` contract to the blockchain ```typescript const deploymentTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Public deployment - abi: storageJson.abi, // ABI of the Storage contract - bytecode: storageJson.bytecode, // Compiled bytecode - function: "", // No constructor arguments - from: owner.lookup, // Account signing the transaction - data: {}, // No additional data + type: TransactionType.PUBLIC, + abi: storageJson.abi, + bytecode: storageJson.bytecode, + from: owner.lookup, + data: {}, }); +// Wait for deployment confirmation const deploymentReceipt = await paladin.pollForReceipt(deploymentTxID, 10000); if (!deploymentReceipt?.contractAddress) { logger.error("Deployment failed!"); return false; } -logger.log("Step 1: Storage contract deployed successfully!"); +logger.log(`Step 1: Storage contract deployed successfully at address: ${deploymentReceipt.contractAddress}`); ``` -- **What happens**: - - The `sendTransaction` function creates a deployment transaction for the `Storage` contract. - - The `pollForReceipt` function waits for the transaction to be confirmed. - - On success, the contract address is returned in the receipt. +### What Happens Here? +1. The `sendTransaction` function **creates a contract deployment transaction** (similar to `eth_sendTransaction`) +2. The `pollForReceipt` function **waits for confirmation** that the contract has been deployed +3. If successful, the **contract address is returned in the receipt** --- -### Step 2: Store a Value +## Step 2: Store a Value -After deploying the contract, you can store a value in the contract using its `store` function. +Now that the contract is deployed, you can **store a value** in it using the `store` function. ```typescript const valueToStore = 125; // Example value to store logger.log(`Step 2: Storing value "${valueToStore}" in the contract...`); const storeTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Public transaction - abi: storageJson.abi, // ABI of the Storage contract - function: "store", // Name of the function to call - from: owner.lookup, // Account signing the transaction - to: deploymentReceipt.contractAddress, // Address of the deployed contract - data: { num: valueToStore }, // Function arguments + type: TransactionType.PUBLIC, + abi: storageJson.abi, + function: "store", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: { num: valueToStore }, }); +// Wait for transaction confirmation const storeReceipt = await paladin.pollForReceipt(storeTxID, 10000); if (!storeReceipt?.transactionHash) { logger.error("Failed to store value in the contract!"); return false; } -logger.log("Step 2: Value stored successfully!"); +logger.log(`Step 2: Value stored successfully in the contract.`); ``` -- **What happens**: - - The `sendTransaction` function sends a transaction to call the `store` function with the value to store (`125` in this example). - - The `pollForReceipt` function waits for the transaction to be confirmed. +### What Happens Here? +1. The `sendTransaction` function **calls the `store` function** with the value `125` +2. The `pollForReceipt` function **waits for confirmation** that the value has been stored --- -### Step 3: Retrieve the Stored Value +## Step 3: Retrieve the Stored Value -Next, retrieve the stored value using the `retrieve` function of the contract. +Now, retrieve the stored value using the `retrieve` function ```typescript logger.log("Step 3: Retrieving the stored value..."); const retrieveResult = await paladin.call({ - type: TransactionType.PUBLIC, // Public call - abi: storageJson.abi, // ABI of the Storage contract - function: "retrieve", // Name of the function to call - from: owner.lookup, // Account making the call - to: deploymentReceipt.contractAddress, // Address of the deployed contract - data: {}, // No arguments required for this function + type: TransactionType.PUBLIC, + abi: storageJson.abi, + function: "retrieve", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: {}, }); -const retrievedValue = retrieveResult["0"]; +const retrievedValue = retrieveResult["value"]; if (retrievedValue !== valueToStore.toString()) { logger.error(`Retrieved value "${retrievedValue}" does not match stored value "${valueToStore}"!`); return false; } -logger.log(`Step 3: Value retrieved successfully! Retrieved value: "${retrievedValue}"`); +logger.log(`Step 3: Value retrieved successfully: "${retrievedValue}"`); ``` -- **What happens**: - - The `call` function retrieves the stored value by interacting with the `retrieve` function of the contract. - - The retrieved value is validated against the previously stored value to ensure correctness. +### What Happens Here? +1. The `call` function **reads the stored value** from the contract (similar to `eth_call`) +2. The retrieved value is **compared to the original stored value** to ensure correctness + +💡 **Transactions (`sendTransaction`) vs. Calls (`call`)** +- `sendTransaction`: **Writes** data to the blockchain (requires a transaction) +- `call`: **Reads** data from the blockchain (does not modify state) + +💡 Why is the number returned as a string? +Paladin follows **JSON-RPC conventions**, where numbers are typically passed as strings to **avoid precision loss in JavaScript** --- ## Conclusion -Congratulations! You’ve successfully: -1. Deployed the `Storage` contract, -2. Stored a value in the contract, and -3. Retrieved the stored value. +🎉 Congratulations! You’ve successfully: + +1. **Deployed** the `Storage` contract +2. **Stored** a value in the contract +3. **Retrieved** the stored value and validated its correctness + +You now understand **how to deploy and interact with a smart contract using the Paladin SDK**, including **JSON-RPC number handling** and **Ethereum transaction conventions** --- ## Next Steps -Now that you've mastered deploying and interacting with a **public storage contract**, it's time to take things to the next level. In the next tutorial, you'll learn about **Storage with Privacy**, where you will add a privacy layer to the blockchain.the blockchain! +Now that you've learned how to deploy a **public storage contract**, it's time to take things to the next level! + +🔒 In the next tutorial, you’ll explore **Storage with Privacy**, where you will **restrict access to stored values using privacy groups** -[Continue to the Privacy Storage Contract Tutorial →](./private-storage.md) \ No newline at end of file +[Continue to the Private Storage Contract Tutorial →](./private-storage.md) \ No newline at end of file diff --git a/example/helloworld/src/index.ts b/example/helloworld/src/index.ts index d6b56174a..6dff219d6 100644 --- a/example/helloworld/src/index.ts +++ b/example/helloworld/src/index.ts @@ -17,12 +17,11 @@ async function main(): Promise { // STEP 1: Deploy the HelloWorld contract logger.log("STEP 1: Deploying the HelloWorld contract..."); const deploymentTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Deploy publicly - abi: helloWorldJson.abi, // ABI of the HelloWorld contract - bytecode: helloWorldJson.bytecode, // Compiled bytecode - function: "", // No constructor arguments in this example - from: owner.lookup, // Account signing and endorsing the transaction - data: {}, // No additional data + type: TransactionType.PUBLIC, + abi: helloWorldJson.abi, + bytecode: helloWorldJson.bytecode, + from: owner.lookup, + data: {}, }); // Wait for the deployment receipt @@ -38,13 +37,13 @@ async function main(): Promise { const name = "Blocky McChainface"; // Example name for the greeting const sayHelloTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Public transaction - abi: helloWorldJson.abi, // ABI of the HelloWorld contract - function: "sayHello", // Name of the function to call - from: owner.lookup, // Account signing and endorsing the transaction - to: deploymentReceipt.contractAddress, // Deployed contract address - data: { // Function arguments - name: name, // Name of the person to greet + type: TransactionType.PUBLIC, + abi: helloWorldJson.abi, + function: "sayHello", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: { + name: name, }, }); @@ -64,9 +63,9 @@ async function main(): Promise { // STEP 3: Retrieve and verify the emitted event logger.log("STEP 3: Retrieving and verifying emitted events..."); const events = await paladin.decodeTransactionEvents( - functionReceipt.transactionHash, // Transaction hash - helloWorldJson.abi, // ABI of the contract - "pretty=true", // encoding format + functionReceipt.transactionHash, + helloWorldJson.abi, + "pretty=true", ); // Extract the event message and validate its content diff --git a/example/notarized-tokens/src/index.ts b/example/notarized-tokens/src/index.ts index 7c429a419..322c1035e 100644 --- a/example/notarized-tokens/src/index.ts +++ b/example/notarized-tokens/src/index.ts @@ -19,8 +19,8 @@ async function main(): Promise { logger.log("Step 1: Deploying a Noto cash token..."); const notoFactory = new NotoFactory(paladinClientNode1, "noto"); const cashToken = await notoFactory.newNoto(verifierNode1, { - notary: verifierNode1, // The notary for this token - restrictMinting: true, // Restrict minting to the notary only + notary: verifierNode1, + notaryMode: "basic" }); if (!cashToken) { logger.error("Failed to deploy the Noto cash token!"); @@ -31,9 +31,9 @@ async function main(): Promise { // Step 2: Mint cash tokens logger.log("Step 2: Minting 2000 units of cash to Node1..."); const mintReceipt = await cashToken.mint(verifierNode1, { - to: verifierNode1, // Mint cash to Node1 - amount: 2000, // Amount to mint - data: "0x", // Optional additional data + to: verifierNode1, + amount: 2000, + data: "0x", }); if (!mintReceipt) { logger.error("Failed to mint cash tokens!"); @@ -44,9 +44,9 @@ async function main(): Promise { // Step 3: Transfer cash to Node2 logger.log("Step 3: Transferring 1000 units of cash from Node1 to Node2..."); const transferToNode2 = await cashToken.transfer(verifierNode1, { - to: verifierNode2, // Transfer to Node2 - amount: 1000, // Amount to transfer - data: "0x", // Optional additional data + to: verifierNode2, + amount: 1000, + data: "0x", }); if (!transferToNode2) { logger.error("Failed to transfer cash to Node2!"); @@ -57,9 +57,9 @@ async function main(): Promise { // Step 4: Transfer cash to Node3 from Node2 logger.log("Step 4: Transferring 800 units of cash from Node2 to Node3..."); const transferToNode3 = await cashToken.using(paladinClientNode2).transfer(verifierNode2, { - to: verifierNode3, // Transfer to Node3 - amount: 800, // Amount to transfer - data: "0x", // Optional additional data + to: verifierNode3, + amount: 800, + data: "0x", }); if (!transferToNode3) { logger.error("Failed to transfer cash to Node3!"); diff --git a/example/privacy-storage/src/helpers/storage.ts b/example/privacy-storage/src/helpers/storage.ts index 76fedc1b2..c99973deb 100644 --- a/example/privacy-storage/src/helpers/storage.ts +++ b/example/privacy-storage/src/helpers/storage.ts @@ -5,10 +5,6 @@ import PaladinClient, { } from "@lfdecentralizedtrust-labs/paladin-sdk"; import storage from "../abis/Storage.json"; -// export interface AddStorageParams { -// addr: string; -// } - export const newPrivateStorage = async ( pente: PentePrivacyGroup, from: PaladinVerifier, @@ -21,7 +17,6 @@ export const newPrivateStorage = async ( return address ? new PrivateStorage(pente, address) : undefined; }; - export class PrivateStorage extends PentePrivateContract<{}> { constructor( protected evm: PentePrivacyGroup, diff --git a/example/privacy-storage/src/index.ts b/example/privacy-storage/src/index.ts index 48c117235..5cf9be9ef 100644 --- a/example/privacy-storage/src/index.ts +++ b/example/privacy-storage/src/index.ts @@ -24,8 +24,8 @@ async function main(): Promise { const penteFactory = new PenteFactory(paladinNode1, "pente"); const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { group: { - salt: newGroupSalt(), // Generate a new salt for the group - members: [verifierNode1, verifierNode2], // Add members to the group + salt: newGroupSalt(), + members: [verifierNode1, verifierNode2], }, evmVersion: "shanghai", endorsementType: "group_scoped_identities", @@ -37,9 +37,9 @@ async function main(): Promise { // Step 2: Deploy a smart contract within the privacy group logger.log("Deploying a smart contract to the privacy group..."); const contractAddress = await memberPrivacyGroup.deploy( - storageJson.abi, // ABI of the contract - storageJson.bytecode, // Bytecode of the contract - verifierNode1 // Deploying as Node1 + storageJson.abi, + storageJson.bytecode, + verifierNode1 ); if (!contractAddress) { @@ -60,14 +60,14 @@ async function main(): Promise { // Retrieve the value as Node1 logger.log("Node1 retrieving the value from the contract..."); const retrievedValueNode1 = await privateStorageContract.call(verifierNode1, "retrieve", []); - logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["0"]); + logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["value"]); // Retrieve the value as Node2 logger.log("Node2 retrieving the value from the contract..."); const retrievedValueNode2 = await privateStorageContract .using(paladinNode2) .call(verifierNode2, "retrieve", []); - logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["0"]); + logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["value"]); // Attempt to retrieve the value as Node3 (outsider) try { diff --git a/example/public-storage/src/index.ts b/example/public-storage/src/index.ts index e176fc5b7..1d95d8549 100644 --- a/example/public-storage/src/index.ts +++ b/example/public-storage/src/index.ts @@ -17,12 +17,11 @@ async function main(): Promise { // Step 1: Deploy the Storage contract logger.log("Step 1: Deploying the Storage contract..."); const deploymentTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Public deployment - abi: storageJson.abi, // ABI of the Storage contract - bytecode: storageJson.bytecode, // Compiled bytecode - function: "", // No constructor arguments - from: owner.lookup, // Account signing the transaction - data: {}, // No additional data + type: TransactionType.PUBLIC, + abi: storageJson.abi, + bytecode: storageJson.bytecode, + from: owner.lookup, + data: {}, }); // Wait for deployment receipt @@ -37,12 +36,12 @@ async function main(): Promise { const valueToStore = 125; // Example value to store logger.log(`Step 2: Storing value "${valueToStore}" in the contract...`); const storeTxID = await paladin.sendTransaction({ - type: TransactionType.PUBLIC, // Public transaction - abi: storageJson.abi, // ABI of the Storage contract - function: "store", // Name of the function to call - from: owner.lookup, // Account signing the transaction - to: deploymentReceipt.contractAddress, // Address of the deployed contract - data: { num: valueToStore }, // Function arguments + type: TransactionType.PUBLIC, + abi: storageJson.abi, + function: "store", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: { num: valueToStore }, }); // Wait for the store transaction receipt @@ -56,16 +55,16 @@ async function main(): Promise { // Step 4: Retrieve the stored value from the contract logger.log("Step 3: Retrieving the stored value..."); const retrieveResult = await paladin.call({ - type: TransactionType.PUBLIC, // Public call - abi: storageJson.abi, // ABI of the Storage contract - function: "retrieve", // Name of the function to call - from: owner.lookup, // Account making the call - to: deploymentReceipt.contractAddress, // Address of the deployed contract - data: {}, // No arguments required for this function + type: TransactionType.PUBLIC, + abi: storageJson.abi, + function: "retrieve", + from: owner.lookup, + to: deploymentReceipt.contractAddress, + data: {}, }); // Validate the retrieved value - const retrievedValue = retrieveResult["0"]; + const retrievedValue = retrieveResult["value"]; if (retrievedValue !== valueToStore.toString()) { logger.error(`Retrieved value "${retrievedValue}" does not match stored value "${valueToStore}"!`); return false; diff --git a/sdk/typescript/src/interfaces/transaction.ts b/sdk/typescript/src/interfaces/transaction.ts index f04d0774a..e1281634a 100644 --- a/sdk/typescript/src/interfaces/transaction.ts +++ b/sdk/typescript/src/interfaces/transaction.ts @@ -16,7 +16,7 @@ export enum TransactionType { export interface ITransactionBase { type: TransactionType; domain?: string; - function: string; + function?: string; from: string; to?: string; data: { diff --git a/solidity/contracts/tutorials/HelloWorld.sol b/solidity/contracts/tutorials/HelloWorld.sol index 4030c1c14..9489a0aea 100644 --- a/solidity/contracts/tutorials/HelloWorld.sol +++ b/solidity/contracts/tutorials/HelloWorld.sol @@ -5,18 +5,19 @@ pragma solidity ^0.8.0; * @title HelloWorld * @dev A simple contract that emits an event with a welcome message */ - contract HelloWorld { - // Define an event + /// @notice Emitted when a user is greeted + /// @param message The generated greeting message event HelloEvent(string message); /** - * @dev Emits an event with a welcome message for the given name - * @param name The name of the person + * @dev Emits a personalized welcome message for the given name + * @param name The name of the recipient */ function sayHello(string memory name) public { - // Format the message using string concatenation - string memory message = string(abi.encodePacked("Welcome to Paladin, ", name, ":)")); - emit HelloEvent(message); // Emit the event with the formatted message + string memory message = string.concat("Welcome to Paladin, ", name, " :)"); + + // Emit the event with the formatted message + emit HelloEvent(message); } } diff --git a/solidity/contracts/tutorials/Storage.sol b/solidity/contracts/tutorials/Storage.sol index ed58ec194..8cd5d6a94 100644 --- a/solidity/contracts/tutorials/Storage.sol +++ b/solidity/contracts/tutorials/Storage.sol @@ -3,26 +3,24 @@ pragma solidity ^0.8.0; /** * @title Storage - * @dev Store & retrieve value in a variable + * @dev Store & retrieve a value in a variable */ - contract Storage { - - uint256 number; + uint256 private _number; /** - * @dev Store value in variable - * @param num value to store + * @dev Store a value in the contract + * @param num The value to store */ function store(uint256 num) public { - number = num; + _number = num; } /** - * @dev Return value - * @return value of 'number' + * @dev Retrieve the stored value + * @return value The currently stored value */ - function retrieve() public view returns (uint256){ - return number; + function retrieve() public view returns (uint256 value) { + return _number; } } From 31d6f21c83edd4ab91479a365faca1de903524aa Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Mon, 10 Feb 2025 15:49:51 -0500 Subject: [PATCH 3/7] updating pente SDK Signed-off-by: David Wertenteil --- doc-site/docs/tutorials/private-storage.md | 98 +++++++++----- .../privacy-storage/src/helpers/storage.ts | 10 +- example/privacy-storage/src/index.ts | 39 ++++-- sdk/typescript/src/domains/pente.ts | 126 ++++++++++-------- 4 files changed, 168 insertions(+), 105 deletions(-) diff --git a/doc-site/docs/tutorials/private-storage.md b/doc-site/docs/tutorials/private-storage.md index 33a4a1ecd..696630381 100644 --- a/doc-site/docs/tutorials/private-storage.md +++ b/doc-site/docs/tutorials/private-storage.md @@ -1,6 +1,6 @@ # Private Storage Contract -In this tutorial, you'll learn how to deploy and interact with a **private storage contract** using Paladin's privacy groups. Unlike the public storage example, here only authorized members of a privacy group can interact with the contract, ensuring secure and private data handling. +In this tutorial, you'll learn how to deploy and interact with a **private storage contract** using **Paladin’s privacy groups**. Unlike the **public storage contract**, where data is visible to everyone, **private storage ensures that only authorized members** of a privacy group can interact with the contract. --- @@ -9,39 +9,36 @@ In this tutorial, you'll learn how to deploy and interact with a **private stora Before starting, make sure you have: 1. Completed the [Public Storage Tutorial](./public-storage.md) and are familiar with: - - Deploying and interacting with contracts. - - Using Paladin SDK for blockchain transactions. -2. A running Paladin network with multiple nodes (at least 3 for this tutorial). + - Deploying and interacting with smart contracts. + - Using the Paladin SDK for blockchain transactions. +2. A **running Paladin network** with at least **three nodes** (Node1, Node2, Node3). --- ## Overview -The `PrivateStorage` tutorial demonstrates how to: +This tutorial will guide you through: -1. Create a **privacy group** with selected members. -2. Deploy a **private Storage contract** within the group. -3. Interact with the contract securely within the group. -4. Test privacy by attempting access from a non-member node. +1. **Creating a privacy group** – Define a **private transaction group** that includes selected members. +2. **Deploying a private contract** – Deploy a **Storage** contract that only privacy group members can interact with. +3. **Interacting with the contract** – Members will **store and retrieve values** securely. +4. **Testing unauthorized access** – A non-member (Node3) will attempt to retrieve data, demonstrating **privacy enforcement**. -The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/private-storage). - -The Solidity contract remains the same as in the [Public Storage Tutorial](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). However, the interaction is scoped to the privacy group. +The **full example** code is available in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/private-storage). --- ## Step 1: Create a Privacy Group -To enable private interactions, start by creating a privacy group with selected members. +To **restrict contract access** to specific members, you first need to create a **privacy group**. ```typescript -// Create a privacy group with Node1 and Node2 logger.log("Creating a privacy group for Node1 and Node2..."); const penteFactory = new PenteFactory(paladinNode1, "pente"); const memberPrivacyGroup = await penteFactory.newPrivacyGroup(verifierNode1, { group: { salt: newGroupSalt(), // Generate a unique salt for the group - members: [verifierNode1, verifierNode2], // Include Node1 and Node2 as members + members: [verifierNode1, verifierNode2], // Only Node1 & Node2 are members }, evmVersion: "shanghai", endorsementType: "group_scoped_identities", @@ -55,19 +52,24 @@ if (!checkDeploy(memberPrivacyGroup)) { logger.log("Privacy group created successfully!"); ``` +#### Key Points: +1. The **privacy group** consists of **Node1 and Node2**. +2. Transactions within this group will **only be visible** to these members. +3. **Node3 is excluded**, meaning it **won’t have access** to private transactions. + --- ## Step 2: Deploy the Contract in the Privacy Group -Deploy the `Storage` contract within the created privacy group. +Now that the **privacy group** is established, **deploy** the `Storage` contract inside this group. ```typescript logger.log("Deploying a private Storage contract..."); -const contractAddress = await memberPrivacyGroup.deploy( - storageJson.abi, // ABI of the Storage contract - storageJson.bytecode, // Bytecode of the Storage contract - verifierNode1 // Deploying as Node1 -); +const contractAddress = await memberPrivacyGroup.deploy({ + abi: storageJson.abi, + bytecode: storageJson.bytecode, + from: verifierNode1.lookup +}); if (!contractAddress) { logger.error("Failed to deploy the private Storage contract."); @@ -76,37 +78,56 @@ if (!contractAddress) { logger.log(`Private Storage contract deployed! Address: ${contractAddress}`); ``` +#### Key Points + +1. The contract is deployed **inside the privacy group**, meaning **only group members** can interact with it. +2. **Transactions involving this contract are private** and only visible to **Node1 & Node2**. + --- ## Step 3: Store and Retrieve Values as Group Members -### Store a Value +### Storing a Value -Group members can store values securely in the private contract. +Now that the contract is deployed, **Node1** can store a value. ```typescript const privateStorage = new PrivateStorage(memberPrivacyGroup, contractAddress); -logger.log("Storing value (125) in the private Storage contract..."); -const storeTx = await privateStorage.invoke(verifierNode1, "store", { num: 125 }); +const valueToStore = 125; // Example value to store +logger.log(`Storing a value "${valueToStore}" in the contract...`); +const storeTx = await privateStorageContract.sendTransaction({ + from: verifierNode1.lookup, + function: "store", + data: { num: valueToStore } +}); logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); ``` --- -### Retrieve the Value as a Member +### Retrieving the Stored Value -Authorized group members can retrieve the stored value. +Group members **Node1 & Node2** can now retrieve the stored value. ```typescript -logger.log("Node1 retrieving the stored value..."); -const retrievedValueNode1 = await privateStorage.call(verifierNode1, "retrieve", []); +// Retrieve the value as Node1 +logger.log("Node1 retrieving the value from the contract..."); +const retrievedValueNode1 = await privateStorageContract.call({ + from: verifierNode1.lookup, + function: "retrieve" + } +); logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["value"]); -logger.log("Node2 retrieving the stored value..."); -const retrievedValueNode2 = await privateStorage +// Retrieve the value as Node2 +logger.log("Node2 retrieving the value from the contract..."); +const retrievedValueNode2 = await privateStorageContract .using(paladinNode2) - .call(verifierNode2, "retrieve", []); + .call({ + from: verifierNode2.lookup, + function: "retrieve", + }); logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["value"]); ``` @@ -114,20 +135,23 @@ logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["value ## Step 4: Verify Privacy by Testing Unauthorized Access -In a **privacy group**, all **inputs and outputs of transactions remain private** among group members. This means that **Node3 (an outsider) cannot reconstruct the contract’s current state** because it was never included in the private state updates. +Now, let’s test if **Node3 (an outsider)** can access the stored data. -Unlike traditional access control mechanisms where permissions are enforced at the contract level, **Paladin’s privacy groups ensure that only the designated members receive and share the necessary state information**. As a result, **Node3 does not have access to any past transactions or stored values, preventing it from reconstructing the contract state**. +### **What should happen?** -### Testing Unauthorized Access +Node3 should NOT be able to retrieve the stored value because it wasn’t part of the privacy group. ```typescript try { logger.log("Node3 (outsider) attempting to retrieve the value..."); - await privateStorage.using(paladinNode3).call(verifierNode3, "retrieve", []); + await privateStorageContract.using(paladinNode3).call({ + from: verifierNode3.lookup, + function: "retrieve", + }); logger.error("Node3 (outsider) should not have access to the private Storage contract!"); return false; } catch (error) { - logger.info("Node3 (outsider) cannot retrieve data because it was never included in the private state updates."); + logger.info("Expected error - Node3 (outsider) cannot retrieve the data. Access denied."); } ``` diff --git a/example/privacy-storage/src/helpers/storage.ts b/example/privacy-storage/src/helpers/storage.ts index c99973deb..1a7807e26 100644 --- a/example/privacy-storage/src/helpers/storage.ts +++ b/example/privacy-storage/src/helpers/storage.ts @@ -9,11 +9,11 @@ export const newPrivateStorage = async ( pente: PentePrivacyGroup, from: PaladinVerifier, ) => { - const address = await pente.deploy( - storage.abi, - storage.bytecode, - from, - ); + const address = await pente.deploy({ + abi: storage.abi, + bytecode: storage.bytecode, + from: from.lookup, + }); return address ? new PrivateStorage(pente, address) : undefined; }; diff --git a/example/privacy-storage/src/index.ts b/example/privacy-storage/src/index.ts index 5cf9be9ef..81225f6e8 100644 --- a/example/privacy-storage/src/index.ts +++ b/example/privacy-storage/src/index.ts @@ -36,11 +36,11 @@ async function main(): Promise { // Step 2: Deploy a smart contract within the privacy group logger.log("Deploying a smart contract to the privacy group..."); - const contractAddress = await memberPrivacyGroup.deploy( - storageJson.abi, - storageJson.bytecode, - verifierNode1 - ); + const contractAddress = await memberPrivacyGroup.deploy({ + abi: storageJson.abi, + bytecode: storageJson.bytecode, + from: verifierNode1.lookup + }); if (!contractAddress) { logger.error("Failed to deploy the contract. No address returned."); @@ -53,32 +53,49 @@ async function main(): Promise { const privateStorageContract = new PrivateStorage(memberPrivacyGroup, contractAddress); // Store a value in the contract - logger.log("Storing a value (125) in the contract..."); - const storeTx = await privateStorageContract.invoke(verifierNode1, "store", { num: 125 }); + const valueToStore = 125; // Example value to store + logger.log(`Storing a value "${valueToStore}" in the contract...`); + const storeTx = await privateStorageContract.sendTransaction({ + from: verifierNode1.lookup, + function: "store", + data: { num: valueToStore } + }); logger.log("Value stored successfully! Transaction hash:", storeTx?.transactionHash); // Retrieve the value as Node1 logger.log("Node1 retrieving the value from the contract..."); - const retrievedValueNode1 = await privateStorageContract.call(verifierNode1, "retrieve", []); + const retrievedValueNode1 = await privateStorageContract.call({ + from: verifierNode1.lookup, + function: "retrieve" + } + ); logger.log("Node1 retrieved the value successfully:", retrievedValueNode1["value"]); // Retrieve the value as Node2 logger.log("Node2 retrieving the value from the contract..."); const retrievedValueNode2 = await privateStorageContract .using(paladinNode2) - .call(verifierNode2, "retrieve", []); + .call({ + from: verifierNode2.lookup, + function: "retrieve", + }); logger.log("Node2 retrieved the value successfully:", retrievedValueNode2["value"]); // Attempt to retrieve the value as Node3 (outsider) try { logger.log("Node3 (outsider) attempting to retrieve the value..."); - await privateStorageContract.using(paladinNode3).call(verifierNode3, "retrieve", []); + await privateStorageContract.using(paladinNode3).call({ + from: verifierNode3.lookup, + function: "retrieve", + }); logger.error("Node3 (outsider) should not have access to the privacy group!"); return false; } catch (error) { - logger.info("Node3 (outsider) cannot retrieve the data from the privacy group. Access denied."); + logger.info("Expected behavior - Node3 (outsider) cannot retrieve the data from the privacy group. Access denied."); } + logger.log("All steps completed successfully!"); + return true; } diff --git a/sdk/typescript/src/domains/pente.ts b/sdk/typescript/src/domains/pente.ts index 250e71e43..d92d12667 100644 --- a/sdk/typescript/src/domains/pente.ts +++ b/sdk/typescript/src/domains/pente.ts @@ -11,6 +11,29 @@ import { PaladinVerifier } from "../verifier"; const DEFAULT_POLL_TIMEOUT = 10000; +export interface PenteGroupTransactionInput { + from: string; + methodAbi: ethers.JsonFragment; + to: string; + data: { + [key: string]: any; + }; +} + +export interface PenteContractTransactionInput { + from: string; + function: string; + data?: { + [key: string]: any; + }; +} + +export interface PenteDeploy { + abi: ReadonlyArray; + bytecode: string, + from: string; + inputs?: any; +} export interface PenteOptions { pollTimeout?: number; } @@ -175,24 +198,27 @@ export class PentePrivacyGroup { } // deploy a contract - async deploy( - abi: ReadonlyArray, - bytecode: string, - from: PaladinVerifier, - inputs?: ConstructorParams - ) { + // async deploy( + // abi: ReadonlyArray, + // bytecode: string, + // from: PaladinVerifier, + // inputs?: ConstructorParams + // ) { + + async deploy(params: PenteDeploy) { // Find the constructor in the ABI - const constructor = abi.find((entry) => entry.type === "constructor"); + const constructor = params.abi.find((entry) => entry.type === "constructor"); // Handle the absence of a constructor const constructorInputs = constructor?.inputs ?? []; + const bytecode = params.bytecode; // Prepare the data object const data: Record = { group: this.group, bytecode, - inputs: inputs ?? [], // Ensure `inputs` is always included, defaulting to an empty array + inputs: params.inputs ?? [], // Ensure `inputs` is always included, defaulting to an empty array }; const txID = await this.paladin.sendTransaction({ @@ -200,7 +226,7 @@ export class PentePrivacyGroup { abi: [privateDeployABI(constructorInputs ?? [])], function: "deploy", to: this.address, - from: from.lookup, + from: params.from, data: data }); const receipt = await this.paladin.pollForReceipt( @@ -214,44 +240,38 @@ export class PentePrivacyGroup { : undefined; } - // invoke functions in the contract - async invoke( - from: PaladinVerifier, - to: string, - methodAbi: ethers.JsonFragment, - inputs: Params - ) { - const txID = await this.paladin.sendTransaction({ - type: TransactionType.PRIVATE, - abi: [privateInvokeABI(methodAbi.name ?? "", methodAbi.inputs ?? [])], - function: "", - to: this.address, - from: from.lookup, - data: { group: this.group, to, inputs }, - }); - return this.paladin.pollForReceipt(txID, this.options.pollTimeout); + // sendTransaction functions in the contract (write) + async sendTransaction(transaction: PenteGroupTransactionInput){ + const inputs = transaction.data; + const to = transaction.to; + const txID = await this.paladin.sendTransaction({ + type: TransactionType.PRIVATE, + abi: [privateInvokeABI(transaction.methodAbi.name ?? "", transaction.methodAbi.inputs ?? [])], + function: "", + to: this.address, + from: transaction.from, + data: { group: this.group, to, inputs }, + }); + return this.paladin.pollForReceipt(txID, this.options.pollTimeout); } // call functions in the contract (read-only) - async call( - from: PaladinVerifier, - to: string, - methodAbi: ethers.JsonFragment, - inputs: Params - ) { + async call(transaction: PenteGroupTransactionInput){ + const to = transaction.to; + const inputs = transaction.data; return this.paladin.call({ type: TransactionType.PRIVATE, abi: [ privateCallABI( - methodAbi.name ?? "", - methodAbi.inputs ?? [], - methodAbi.outputs ?? [] + transaction.methodAbi.name ?? "", + transaction.methodAbi.inputs ?? [], + transaction.methodAbi.outputs ?? [] ), ], function: "", to: this.address, - from: from.lookup, - data: { group: this.group, to, inputs }, + from: transaction.from, + data: { group: this.group, to, inputs } }); } @@ -282,27 +302,29 @@ export abstract class PentePrivateContract { paladin: PaladinClient ): PentePrivateContract; - async invoke( - from: PaladinVerifier, - methodName: string, - params: Params - ) { - const method = this.abi.find((entry) => entry.name === methodName); + async sendTransaction(transaction: PenteContractTransactionInput){ + const method = this.abi.find((entry) => entry.name === transaction.function); if (method === undefined) { - throw new Error(`Method '${methodName}' not found`); + throw new Error(`Method '${transaction.function}' not found`); } - return this.evm.invoke(from, this.address, method, params); + return this.evm.sendTransaction({ + from: transaction.from, + to: this.address, + methodAbi: method, + data: transaction.data ?? [] + }); } - async call( - from: PaladinVerifier, - methodName: string, - params: Params - ) { - const method = this.abi.find((entry) => entry.name === methodName); + async call(transaction: PenteContractTransactionInput){ + const method = this.abi.find((entry) => entry.name === transaction.function); if (method === undefined) { - throw new Error(`Method '${methodName}' not found`); + throw new Error(`Method '${transaction.function}' not found`); } - return this.evm.call(from, this.address, method, params); + return this.evm.call({ + from: transaction.from, + to: this.address, + methodAbi: method, + data: transaction.data ?? [] + }); } } From f3a6d02076816013597a09c655c4fd5e5e8c3178 Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Mon, 10 Feb 2025 16:27:54 -0500 Subject: [PATCH 4/7] bond example Signed-off-by: David Wertenteil --- example/bond/src/helpers/bondsubscription.ts | 29 ++++++++++++++------ example/bond/src/helpers/bondtracker.ts | 23 ++++++++++------ example/bond/src/helpers/investorlist.ts | 6 +++- example/bond/src/index.ts | 2 +- sdk/typescript/src/domains/pente.ts | 8 ------ 5 files changed, 41 insertions(+), 27 deletions(-) diff --git a/example/bond/src/helpers/bondsubscription.ts b/example/bond/src/helpers/bondsubscription.ts index 6e50c0434..47dfc6959 100644 --- a/example/bond/src/helpers/bondsubscription.ts +++ b/example/bond/src/helpers/bondsubscription.ts @@ -34,12 +34,12 @@ export const newBondSubscription = async ( if (bondSubscriptionConstructor === undefined) { throw new Error("Bond subscription constructor not found"); } - const address = await pente.deploy( - bondSubscription.abi, - bondSubscription.bytecode, - from, - params - ); + const address = await pente.deploy({ + abi: bondSubscription.abi, + bytecode: bondSubscription.bytecode, + from: from.lookup, + inputs: params + }); return address ? new BondSubscription(pente, address) : undefined; }; @@ -56,14 +56,25 @@ export class BondSubscription extends PentePrivateContract { - const address = await pente.deploy( - bondTracker.abi, - bondTracker.bytecode, - from, - params - ); + const address = await pente.deploy({ + abi: bondTracker.abi, + bytecode: bondTracker.bytecode, + from: from.lookup, + inputs: params + }); return address ? new BondTracker(pente, address) : undefined; }; @@ -45,11 +45,18 @@ export class BondTracker extends PentePrivateContract { } addInvestor(from: PaladinVerifier, params: AddInvestorParams) { - return this.invoke(from, "addInvestor", params); + return this.sendTransaction({ + from: from.lookup, + function: "addInvestor", + data: params + }); } } diff --git a/example/bond/src/index.ts b/example/bond/src/index.ts index e139312bf..269789fdb 100644 --- a/example/bond/src/index.ts +++ b/example/bond/src/index.ts @@ -243,7 +243,7 @@ async function main(): Promise { logger.error("Prepared bond transfer had no 'to' address"); return false; } - if (!bondTransfer2.transaction.function.startsWith("transition(")) { + if (!bondTransfer2.transaction.function?.startsWith("transition(")) { logger.error( `Prepared bond transfer did not seem to be a Pente transition: ${bondTransfer2.transaction}` ); diff --git a/sdk/typescript/src/domains/pente.ts b/sdk/typescript/src/domains/pente.ts index d92d12667..82a00c369 100644 --- a/sdk/typescript/src/domains/pente.ts +++ b/sdk/typescript/src/domains/pente.ts @@ -196,14 +196,6 @@ export class PentePrivacyGroup { this.options ); } - - // deploy a contract - // async deploy( - // abi: ReadonlyArray, - // bytecode: string, - // from: PaladinVerifier, - // inputs?: ConstructorParams - // ) { async deploy(params: PenteDeploy) { From 4ffc9fb7e4436f3368c6e4eddd4d10901354806a Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Wed, 12 Feb 2025 09:46:02 -0500 Subject: [PATCH 5/7] fixed swap Signed-off-by: David Wertenteil --- example/swap/src/helpers/erc20tracker.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/swap/src/helpers/erc20tracker.ts b/example/swap/src/helpers/erc20tracker.ts index 8171613e1..8575fdfc6 100644 --- a/example/swap/src/helpers/erc20tracker.ts +++ b/example/swap/src/helpers/erc20tracker.ts @@ -15,12 +15,12 @@ export const newERC20Tracker = async ( from: PaladinVerifier, params: ERC20TrackerConstructorParams ) => { - const address = await pente.deploy( - erc20Tracker.abi, - erc20Tracker.bytecode, - from, - params - ); + const address = await pente.deploy({ + abi: erc20Tracker.abi, + bytecode: erc20Tracker.bytecode, + from: from.lookup, + inputs: params, + }); return address ? new BondTracker(pente, address) : undefined; }; From 4922117e7242da8e85c1bc5256e6441dee5cb0a8 Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Wed, 12 Feb 2025 11:14:55 -0500 Subject: [PATCH 6/7] Rename name example Signed-off-by: David Wertenteil --- example/helloworld/src/index.ts | 6 +++--- solidity/contracts/tutorials/HelloWorld.sol | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/example/helloworld/src/index.ts b/example/helloworld/src/index.ts index 6dff219d6..803603798 100644 --- a/example/helloworld/src/index.ts +++ b/example/helloworld/src/index.ts @@ -34,7 +34,7 @@ async function main(): Promise { // STEP 2: Call the sayHello function logger.log("STEP 2: Calling the sayHello function..."); - const name = "Blocky McChainface"; // Example name for the greeting + const name = "John"; // Example name for the greeting const sayHelloTxID = await paladin.sendTransaction({ type: TransactionType.PUBLIC, @@ -70,9 +70,9 @@ async function main(): Promise { // Extract the event message and validate its content const message = events[0].data["message"]; - const expectedOutput = `Welcome to Paladin, ${name}:)`; + const expectedOutput = `Welcome to Paladin, ${name}`; if (message !== expectedOutput) { - logger.error("STEP 3: Event data does not match the expected output!"); + logger.error(`STEP 3: ERROR - Event data does not match the expected output! message: "${message}"`); return false; } logger.log("STEP 3: Events verified successfully!"); diff --git a/solidity/contracts/tutorials/HelloWorld.sol b/solidity/contracts/tutorials/HelloWorld.sol index 9489a0aea..c1d6895b8 100644 --- a/solidity/contracts/tutorials/HelloWorld.sol +++ b/solidity/contracts/tutorials/HelloWorld.sol @@ -15,7 +15,7 @@ contract HelloWorld { * @param name The name of the recipient */ function sayHello(string memory name) public { - string memory message = string.concat("Welcome to Paladin, ", name, " :)"); + string memory message = string.concat("Welcome to Paladin, ", name); // Emit the event with the formatted message emit HelloEvent(message); From ddda54b06574f6cd70c70f4a26e743ac66ec4bb3 Mon Sep 17 00:00:00 2001 From: David Wertenteil Date: Wed, 19 Feb 2025 12:15:05 -0500 Subject: [PATCH 7/7] Addressed PR comments Signed-off-by: David Wertenteil --- doc-site/docs/tutorials/hello-world.md | 4 ++-- doc-site/docs/tutorials/notarized-tokens.md | 10 +++++----- doc-site/docs/tutorials/private-storage.md | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/doc-site/docs/tutorials/hello-world.md b/doc-site/docs/tutorials/hello-world.md index 054202ef9..29325e298 100644 --- a/doc-site/docs/tutorials/hello-world.md +++ b/doc-site/docs/tutorials/hello-world.md @@ -9,9 +9,9 @@ This tutorial walks you through deploying and interacting with a simple `HelloWo ## Running the Example -The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/public-storage). +The example code can be found in the [Paladin example repository](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/helloworld). -The storage solidity contract can be found [here](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/Storage.sol). +The HelloWorld solidity contract can be found [here](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/solidity/contracts/tutorial/HelloWorld.sol). Follow the [Getting Started](../../getting-started/installation/) instructions to set up a Paladin environment. Then, follow the example [README](https://github.com/LF-Decentralized-Trust-labs/paladin/blob/main/example/helloworld/README.md) to run the code. diff --git a/doc-site/docs/tutorials/notarized-tokens.md b/doc-site/docs/tutorials/notarized-tokens.md index 40fb33f0d..1bb162f98 100644 --- a/doc-site/docs/tutorials/notarized-tokens.md +++ b/doc-site/docs/tutorials/notarized-tokens.md @@ -41,12 +41,12 @@ First, create a **Noto Factory** instance and deploy a new token. **Node1** will logger.log("Step 1: Deploying a Noto cash token..."); const notoFactory = new NotoFactory(paladinClientNode1, "noto"); const cashToken = await notoFactory.newNoto(verifierNode1, { -notary: verifierNode1,// The notary overseeing ALL token transactions -notaryMode: "basic",// The notary mode + notary: verifierNode1, + notaryMode: "basic", }); if (!cashToken) { -logger.error("Failed to deploy the Noto cash token!"); -return false; + logger.error("Failed to deploy the Noto cash token!"); + return false; } logger.log("Noto cash token deployed successfully!"); ``` @@ -82,7 +82,7 @@ logger.log("Successfully minted 2000 units of cash to Node1!"); ### What Happens Here? -1. **Node1 submits a minting request** to the notary. +1. **Node1 submits a minting request** to the notary (in this case, node1 is the notary so it will be receiving and validating it's own request). 2. **The notary reviews and approves** the request. 3. **Tokens are minted and assigned** to the recipient. 4. **The `data` field is recorded** in the transaction receipt for auditability. diff --git a/doc-site/docs/tutorials/private-storage.md b/doc-site/docs/tutorials/private-storage.md index 696630381..c09e99c43 100644 --- a/doc-site/docs/tutorials/private-storage.md +++ b/doc-site/docs/tutorials/private-storage.md @@ -2,6 +2,8 @@ In this tutorial, you'll learn how to deploy and interact with a **private storage contract** using **Paladin’s privacy groups**. Unlike the **public storage contract**, where data is visible to everyone, **private storage ensures that only authorized members** of a privacy group can interact with the contract. +If you're new to **Pente privacy groups** or want to dive deeper into their architecture, check out the [Pente documentation](https://lf-decentralized-trust-labs.github.io/paladin/head/architecture/pente) for more information. + --- ## Prerequisites