Skip to content

Commit

Permalink
build(connector-stellar): add a run soroban transaction endpoint
Browse files Browse the repository at this point in the history
- Add a run soroban transaction endpoint to the Stellar Connector plugin.
- Add a Soroban hellow_world contract to the demo-contract folder.

**Remarks:**

The `runSorobanTransaction` endpoint can be used to make smart contract
invocations on the Soroban platform. The endpoint accepts a flag
called `readOnly` to indicate when the transaction should not alter
ledger state. When `true`, the transaction will only be simulated based
on the current ledger state and provide an up-to-date output without
registering the transaction to the ledger, ensuring no fees are
consumed. When `false`, the transaction will be executed and registered
to the ledger even if it doesn't alter the ledger state, incurring
fees as usual.

More details can be found in the `README.md` file under the connector root directory.

Closes hyperledger#3238

Signed-off-by: Fabricius Zatti <fazzatti@gmail.com>
  • Loading branch information
fazzatti committed Jun 10, 2024
1 parent cc2f9c5 commit e1206b0
Show file tree
Hide file tree
Showing 19 changed files with 4,200 additions and 14 deletions.
76 changes: 68 additions & 8 deletions packages/cacti-plugin-ledger-connector-stellar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
This plugin provides `Cacti` a way to interact with Stellar networks. Using this we can perform:

- Deploy Smart-contracts over the network.
- Build and sign transactions. (WIP)
- Invoke smart-contract functions that we have deployed on the network. (WIP)
- Build and sign transactions.
- Invoke smart-contract functions that we have deployed on the network.

## Summary

Expand Down Expand Up @@ -51,11 +51,28 @@ This endpoint is responsible for deploying smart contracts to Soroban (Stellar's
**Core Aspects**:

- **Input**: Accepts either a compiled WASM buffer or a WASM hash.
- **WASM Buffer**: Uploads compiled code to Stellar, generates a on-chain WASM hash, then deploys the contract. Refer to the [Stellar documentation](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/contracts) for further detail on this process.
- **WASM Hash**: Directly deploys the contract using the existing WASM hash.

- `wasmBuffer`: Uploads compiled code to Stellar, generates a on-chain WASM hash, then deploys the contract. Refer to the [Stellar documentation](https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structures/contracts) for further detail on this process.
- `wasmHash`: Directly deploys the contract using the existing WASM hash.
- `transactionInvocation`: An object containing data about how the transaction should be assembled and executed.

- **Output**: An object containing the on-chain WASM hash and a unique contract ID of the newly deployed instance.

- **Transaction Invocation**: Provides data for assembling and executing the transactions.
#### `run-soroban-transaction` endpoint

This endpoint is responsible for invoking smart contracts on Soroban (Stellar's smart contract platform) to either change or read a ledger state.

**Core Aspects**:

- **Input**: Accepts either a compiled WASM buffer or a WASM hash.
- `contractId`: The unique contract id of the contract instance to be invoked.
- `method`: The name of the contract method being invoked.
- `methodArgs`: An object containing the arguments accepted by the method.
- `specXdr`: An array containing the contract specification in XDR format.
- `transactionInvocation`: An object containing data about how the transaction should be assembled and executed.
- `readOnly`: A flag to indicate when the transaction should not alter ledger state. When `true`, the transaction will only be simulated based on the current ledger state and provide an up-to-date output without registering the transaction to the ledger, ensuring no fees are consumed.
- **Output**: An object containing the response of the transaction invocation.
- `result`: The direct output of the invoked contract method.

### Usage

Expand Down Expand Up @@ -111,6 +128,28 @@ const deployOut = await connector.deployContract({
});
```

Call example to invoke a contract and return the output value:

```typescript
const res = await connector.runSorobanTransaction({
contractId,
method: "balance",
methodArgs: {
id: adminAccount.getPublicKey(),
},
specXdr: tokenSpec,
readOnly: true,
transactionInvocation: {
header: {
source: adminAccount.getPublicKey(),
fee: 100,
timeout: 30,
},
signers: [adminAccount.getSecretKey()],
},
});
```

### Building/running the container image locally

In the Cacti project root say:
Expand Down Expand Up @@ -168,9 +207,30 @@ docker run \

#### Testing API calls with the container

Don't have a Stellar network on hand to test with? Test or develop against our Stellar All-In-One container!
Don't have a Stellar network on hand to test with? Test or develop against our Stellar All-In-One container by importing the `StellarTestLedger` from `cacti-test-tooling`. It will deploy and manage a docker image based on [Stellar quickstart](https://github.com/stellar/quickstart).

**Usage Example**(refer to the integration tests for further examples):

```typescript
import { StellarTestLedger } from "@hyperledger/cactus-test-tooling";
import { Network } from "stellar-plus/lib/stellar-plus";

const logLevel: LogLevelDesc = "TRACE";
const stellarTestLedger = new StellarTestLedger({ logLevel });

await stellarTestLedger.start();
const networkConfig = Network.CustomNet(
await stellarTestLedger.getNetworkConfiguration(),
);

// Here use the networkConfig object to connect to
// your test ledger and run your tests.

await stellarTestLedger.stop();
await stellarTestLedger.destroy();
```

TODO (WIP)
In this example, the `StellarTestLedger` is used to pull up a fresh new ledger with no history and all of its required services to interact with the network. In conjunction with the `stellar-plus` library, the method `getNetworkConfiguration` is used to get all services data and instantiate an object that can be used with all tools in Stellar Plus to directly interact with the test ledger.

## Prometheus Exporter

Expand All @@ -184,7 +244,7 @@ You can also initialize the prometheus exporter object seperately and then pass
`getPrometheusMetricsV1` function returns the prometheus exporter metrics, currently displaying the total transaction count, which currently increments everytime a Stellar transaction is executed through the connector internal methods. This includes the methods

- `deployContract`
- `runSorobanTransaction()` (_Soon_)
- `runSorobanTransaction()`

### Prometheus Integration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,66 @@
}
},

"RunSorobanTransactionRequest": {
"type": "object",
"additionalProperties": false,
"required": [
"contractId",
"method",
"specXdr",
"transactionInvocation",
"readOnly"
],
"properties": {
"contractId": {
"type": "string",
"pattern": "^C[a-zA-Z0-9]{55}$",
"nullable": false,
"minLength": 56,
"maxLength": 56,
"description": "The ID of the contract that was deployed."
},
"specXdr": {
"type": "array",
"items": {
"type": "string"
},
"nullable": false,
"description": "Array of strings containing the XDR of the contract specification."
},
"method": {
"type": "string",
"nullable": false,
"description": "The method to be called on the contract."
},
"methodArgs": {
"type": "object",
"nullable": true,
"description": "The arguments to pass to the method."
},
"transactionInvocation": {
"$ref": "#/components/schemas/TransactionInvocation",
"nullable": false,
"description": "The transaction invocation that will be used to run the contract."
},
"readOnly": {
"type": "boolean",
"nullable": false,
"description": "Flag indicating if the transaction should be read-only."
}
}
},
"RunSorobanTransactionResponse": {
"type": "object",
"properties": {
"result": {
"type": "object",
"description": "The result of the invoked contract method."
}
},
"description": "Response object containing the result of a contract invocation or error information if it failed."
},

"DeployContractV1Request": {
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -211,6 +271,41 @@
}
},

"/api/v1/plugins/@hyperledger/cacti-plugin-ledger-connector-stellar/run-soroban-transaction": {
"post": {
"x-hyperledger-cacti": {
"http": {
"verbLowerCase": "post",
"path": "/api/v1/plugins/@hyperledger/cacti-plugin-ledger-connector-stellar/run-soroban-transaction"
}
},
"operationId": "runSorobanTransactionV1",
"summary": "Executes a Soroban transaction on a stellar ledger",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunSorobanTransactionRequest"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RunSorobanTransactionResponse"
}
}
}
}
}
}
},

"/api/v1/plugins/@hyperledger/cacti-plugin-ledger-connector-stellar/get-prometheus-exporter-metrics": {
"get": {
"x-hyperledger-cacti": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ContractEngine } from "stellar-plus/lib/stellar-plus/core/contract-engi
import { SorobanTransactionPipelineOptions } from "stellar-plus/lib/stellar-plus/core/pipelines/soroban-transaction/types";
import { NetworkConfig } from "stellar-plus/lib/stellar-plus/network";
import { TransactionInvocation } from "stellar-plus/lib/stellar-plus/types";
import { Core as SPCore } from "stellar-plus/lib/stellar-plus/core/index";

export interface DeployContractWithWasmOptions
extends BaseContractEngineInvocation {
Expand All @@ -25,6 +26,21 @@ interface BaseContractEngineInvocation {
networkConfig: NetworkConfig;
}

export interface InvokeContractOptions<T> extends BaseInvokeContractOptions<T> {
readOnly: false;
}
export interface ReadContractOptions<T> extends BaseInvokeContractOptions<T> {
readOnly: true;
}

export interface BaseInvokeContractOptions<T>
extends BaseContractEngineInvocation {
contractId: string;
method: string;
methodArgs: T;
specXdr: string[];
}

/**
* Deploys a contract to the Stellar network. Accepts either a WebAssembly binary buffer or a hash of the WebAssembly binary code in the ledger.
*
Expand Down Expand Up @@ -76,3 +92,78 @@ export const deployContract = async (

return output;
};

/**
*
* Invokes a contract on the Stellar network. Can perform a full contract invocation or a read-only contract invocation.
* Read-only should be used when the contract does not modify the ledger state. In these cases, the contract invocation is not submitted to the network.
* Only a simulation is performed to determine the result of the contract invocation, therefore no fees are incurred.
*
* @param {InvokeContractOptions<T> | ReadContractOptions<T>} options - Options for invoking a contract.
* @param {TransactionInvocation} options.txInvocation - Transaction invocation object containing the parameters to configure the transaction envelope.
* @param {SorobanTransactionPipelineOptions} [options.pipelineOptions] - Options for the Soroban transaction pipeline.
* @param {string} options.fnLogPrefix - Prefix for log messages.
* @param {NetworkConfig} options.networkConfig - Network configuration object. Contains the details of the services available to interact with the Stellar network.
* @param {string} options.contractId - Contract ID of the contract to invoke.
* @param {string} options.method - Method to invoke on the contract.
* @param {T} options.methodArgs - Object containing the arguments to pass to the method.
* @param {string[]} options.specXdr - Array of strings containing the XDR of the contract specification.
* @param {boolean} options.readOnly - Flag to indicate if the contract invocation is read-only.
*
* @returns {Promise<unknown>} - Returns the result of the contract invocation.
*
* @throws {Error} - Throws an error if the contract invocation fails.
* @throws {Error} - Throws an error if the contract read fails.
*
*
* @returns
*/
export const invokeContract = async <T>(
options: InvokeContractOptions<T> | ReadContractOptions<T>,
): Promise<unknown> => {
const {
networkConfig,
txInvocation,
fnLogPrefix,
pipelineOptions,
contractId,
method,
methodArgs,
specXdr,
} = options;

const fnTag = `${fnLogPrefix}#invokeContract()`;

const contractEngine = new ContractEngine({
networkConfig,
contractParameters: {
contractId: contractId,
spec: new SPCore.ContractSpec(specXdr),
},
options: {
sorobanTransactionPipeline: pipelineOptions,
},
});

if (options.readOnly) {
return await contractEngine
.readFromContract({
method,
methodArgs: methodArgs as object,
...txInvocation,
})
.catch((error) => {
throw new Error(`${fnTag} Failed to read contract. ` + error);
});
}

return await contractEngine
.invokeContract({
method,
methodArgs: methodArgs as object,
...txInvocation,
})
.catch((error) => {
throw new Error(`${fnTag} Failed to invoke contract. ` + error);
});
};
Loading

0 comments on commit e1206b0

Please sign in to comment.