Skip to content

Commit

Permalink
Soroban counter.sol example (#1645)
Browse files Browse the repository at this point in the history
This PR aims to make Solang support a simple counter.sol example on
Soroban, where a storage variable is instatiated, modified and
retrieved. The counter contract is only limited to `uint64` data types,
and only supports `instance` soroban storage.

This can be considered a "skeleton" for supporting more data and storage
types, as well as more host function invokations.

- [x] Support Soroban storage function calls `put_contract_data`,
`get_contract_data` and `has_contract_data`
- [x] Implement a wrapper `init` for `storage_initializer` 
- [x] Implement wrappers for public functions
- [x] Insert decoding/encoding instructions into the wrapper functions 
- [x] Soroban doesn't have function return codes. This needs to be
handled all over emit
- [x] Add integration tests and MockVm tests

---------

Signed-off-by: salaheldinsoliman <[email protected]>
  • Loading branch information
salaheldinsoliman committed Jun 25, 2024
1 parent 08dbe49 commit 399c199
Show file tree
Hide file tree
Showing 25 changed files with 794 additions and 126 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,48 @@ jobs:
with:
name: anchor-tests
path: ./target/*.profraw

soroban:
name: Soroban Integration test
runs-on: solang-ubuntu-latest
container: ghcr.io/hyperledger/solang-llvm:ci-7
needs: linux-x86-64
steps:
- name: Checkout sources
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
- uses: dtolnay/[email protected]
- uses: actions/download-artifact@v3
with:
name: solang-linux-x86-64
path: bin
- name: Solang Compiler
run: |
chmod 755 ./bin/solang
echo "$(pwd)/bin" >> $GITHUB_PATH
- name: Install Soroban
run: cargo install --locked soroban-cli --version 21.0.0-rc.1
- name: Add cargo install location to PATH
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- run: npm install
working-directory: ./integration/soroban
- name: Build Solang contracts
run: npm run build
working-directory: ./integration/soroban
- name: Setup Soroban enivronment
run: npm run setup
working-directory: ./integration/soroban
- name: Deploy and test contracts
run: npm run test
working-directory: ./integration/soroban
- name: Upload test coverage files
uses: actions/[email protected]
with:
name: soroban-tests
path: ./target/*.profraw

solana:
name: Solana Integration test
Expand Down
8 changes: 8 additions & 0 deletions integration/soroban/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.js
*.so
*.key
*.json
!tsconfig.json
!package.json
node_modules
package-lock.json
13 changes: 13 additions & 0 deletions integration/soroban/counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
contract counter {
uint64 public count = 10;

function increment() public returns (uint64) {
count += 1;
return count;
}

function decrement() public returns (uint64) {
count -= 1;
return count;
}
}
55 changes: 55 additions & 0 deletions integration/soroban/counter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as StellarSdk from '@stellar/stellar-sdk';
import { readFileSync } from 'fs';
import { expect } from 'chai';
import path from 'path';
import { fileURLToPath } from 'url';
import { call_contract_function } from './test_helpers.js';

const __filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(__filename);

describe('Counter', () => {
let keypair;
const server = new StellarSdk.SorobanRpc.Server(
"https://soroban-testnet.stellar.org:443",
);

let contractAddr;
let contract;
before(async () => {

console.log('Setting up counter contract tests...');

// read secret from file
const secret = readFileSync('alice.txt', 'utf8').trim();
keypair = StellarSdk.Keypair.fromSecret(secret);

let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'counter.txt');
// read contract address from file
contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString();

// load contract
contract = new StellarSdk.Contract(contractAddr);

// initialize the contract
await call_contract_function("init", server, keypair, contract);

});

it('get correct initial counter', async () => {
// get the count
let count = await call_contract_function("count", server, keypair, contract);
expect(count.toString()).eq("10");
});

it('increment counter', async () => {
// increment the counter
await call_contract_function("increment", server, keypair, contract);

// get the count
let count = await call_contract_function("count", server, keypair, contract);
expect(count.toString()).eq("11");
});
});


23 changes: 23 additions & 0 deletions integration/soroban/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"type": "module",
"dependencies": {
"@stellar/stellar-sdk": "^12.0.1",
"chai": "^5.1.1",
"dotenv": "^16.4.5",
"mocha": "^10.4.0"
},
"scripts": {
"build": "solang compile *.sol --target soroban",
"setup": "node setup.js",
"test": "mocha *.spec.js --timeout 20000"
},
"devDependencies": {
"@eslint/js": "^9.4.0",
"@types/mocha": "^10.0.6",
"eslint": "^9.4.0",
"expect": "^29.7.0",
"globals": "^15.4.0",
"typescript": "^5.4.5"
}
}

63 changes: 63 additions & 0 deletions integration/soroban/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

import 'dotenv/config';
import { mkdirSync, readdirSync} from 'fs';
import { execSync } from 'child_process';
import path from 'path';
import { fileURLToPath } from 'url';

console.log("###################### Initializing ########################");

// Get dirname (equivalent to the Bash version)
const __filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(__filename);

// variable for later setting pinned version of soroban in "$(dirname/target/bin/soroban)"
const soroban = "soroban"

// Function to execute and log shell commands
function exe(command) {
console.log(command);
execSync(command, { stdio: 'inherit' });
}

function generate_alice() {
exe(`${soroban} keys generate alice --network testnet`);

// get the secret key of alice and put it in alice.txt
exe(`${soroban} keys show alice > alice.txt`);
}


function filenameNoExtension(filename) {
return path.basename(filename, path.extname(filename));
}

function deploy(wasm) {

let contractId = path.join(dirname, '.soroban', 'contract-ids', filenameNoExtension(wasm) + '.txt');

exe(`(${soroban} contract deploy --wasm ${wasm} --ignore-checks --source-account alice --network testnet) > ${contractId}`);
}

function deploy_all() {
const contractsDir = path.join(dirname, '.soroban', 'contract-ids');
mkdirSync(contractsDir, { recursive: true });

const wasmFiles = readdirSync(`${dirname}`).filter(file => file.endsWith('.wasm'));

wasmFiles.forEach(wasmFile => {
deploy(path.join(dirname, wasmFile));
});
}

function add_testnet() {

exe(`${soroban} network add \
--global testnet \
--rpc-url https://soroban-testnet.stellar.org:443 \
--network-passphrase "Test SDF Network ; September 2015"`);
}

add_testnet();
generate_alice();
deploy_all();
53 changes: 53 additions & 0 deletions integration/soroban/test_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as StellarSdk from '@stellar/stellar-sdk';



export async function call_contract_function(method, server, keypair, contract) {

let res;
let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
}).addOperation(contract.call(method)).setTimeout(30).build();

let preparedTransaction = await server.prepareTransaction(builtTransaction);

// Sign the transaction with the source account's keypair.
preparedTransaction.sign(keypair);

try {
let sendResponse = await server.sendTransaction(preparedTransaction);
if (sendResponse.status === "PENDING") {
let getResponse = await server.getTransaction(sendResponse.hash);
// Poll `getTransaction` until the status is not "NOT_FOUND"
while (getResponse.status === "NOT_FOUND") {
console.log("Waiting for transaction confirmation...");
// See if the transaction is complete
getResponse = await server.getTransaction(sendResponse.hash);
// Wait one second
await new Promise((resolve) => setTimeout(resolve, 1000));
}

if (getResponse.status === "SUCCESS") {
// Make sure the transaction's resultMetaXDR is not empty
if (!getResponse.resultMetaXdr) {
throw "Empty resultMetaXDR in getTransaction response";
}
// Find the return value from the contract and return it
let transactionMeta = getResponse.resultMetaXdr;
let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
console.log(`Transaction result: ${returnValue.value()}`);
res = returnValue.value();
} else {
throw `Transaction failed: ${getResponse.resultXdr}`;
}
} else {
throw sendResponse.errorResultXdr;
}
} catch (err) {
// Catch and report any errors we've thrown
console.log("Sending transaction failed");
console.log(err);
}
return res;
}
16 changes: 15 additions & 1 deletion src/bin/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,15 @@ pub struct CompilePackage {
#[arg(name = "VERSION", help = "specify contracts version", long = "version", num_args = 1, value_parser = ValueParser::new(parse_version))]
#[serde(default, deserialize_with = "deserialize_version")]
pub version: Option<String>,

#[arg(
name = "SOROBAN-VERSION",
help = "specify soroban contracts pre-release number",
short = 's',
long = "soroban-version",
num_args = 1
)]
pub soroban_version: Option<u64>,
}

#[derive(Args, Deserialize, Debug, PartialEq)]
Expand Down Expand Up @@ -545,7 +554,11 @@ pub fn imports_arg<T: PackageTrait>(package: &T) -> FileResolver {
resolver
}

pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Options {
pub fn options_arg(
debug: &DebugFeatures,
optimizations: &Optimizations,
compiler_inputs: &CompilePackage,
) -> Options {
let opt_level = if let Some(level) = &optimizations.opt_level {
match level.as_str() {
"none" => OptimizationLevel::None,
Expand Down Expand Up @@ -574,6 +587,7 @@ pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Opti
} else {
None
}),
soroban_version: compiler_inputs.soroban_version,
}
}

Expand Down
18 changes: 15 additions & 3 deletions src/bin/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,17 @@ mod tests {

let default_optimize: cli::Optimizations = toml::from_str("").unwrap();

let opt = options_arg(&default_debug, &default_optimize);
let compiler_package = cli::CompilePackage {
input: Some(vec![PathBuf::from("flipper.sol")]),
contracts: Some(vec!["flipper".to_owned()]),
import_path: Some(vec![]),
import_map: Some(vec![]),
authors: None,
version: Some("0.1.0".to_string()),
soroban_version: None,
};

let opt = options_arg(&default_debug, &default_optimize, &compiler_package);

assert_eq!(opt, Options::default());

Expand Down Expand Up @@ -185,7 +195,8 @@ mod tests {
import_path: Some(vec![]),
import_map: Some(vec![]),
authors: None,
version: Some("0.1.0".to_string())
version: Some("0.1.0".to_string()),
soroban_version: None
},
compiler_output: cli::CompilerOutput {
emit: None,
Expand Down Expand Up @@ -239,7 +250,8 @@ mod tests {
import_path: Some(vec![]),
import_map: Some(vec![]),
authors: Some(vec!["not_sesa".to_owned()]),
version: Some("0.1.0".to_string())
version: Some("0.1.0".to_string()),
soroban_version: None
},
compiler_output: cli::CompilerOutput {
emit: None,
Expand Down
8 changes: 7 additions & 1 deletion src/bin/solang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,13 @@ fn compile(compile_args: &Compile) {

let mut resolver = imports_arg(&compile_args.package);

let opt = options_arg(&compile_args.debug_features, &compile_args.optimizations);
let compile_package = &compile_args.package;

let opt = options_arg(
&compile_args.debug_features,
&compile_args.optimizations,
compile_package,
);

let mut namespaces = Vec::new();

Expand Down
5 changes: 3 additions & 2 deletions src/codegen/dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ use crate::{sema::ast::Namespace, Target};

pub(crate) mod polkadot;
pub(super) mod solana;
pub(super) mod soroban;

pub(super) fn function_dispatch(
contract_no: usize,
all_cfg: &[ControlFlowGraph],
all_cfg: &mut [ControlFlowGraph],
ns: &mut Namespace,
opt: &Options,
) -> Vec<ControlFlowGraph> {
Expand All @@ -17,6 +18,6 @@ pub(super) fn function_dispatch(
Target::Polkadot { .. } | Target::EVM => {
polkadot::function_dispatch(contract_no, all_cfg, ns, opt)
}
Target::Soroban => vec![],
Target::Soroban => soroban::function_dispatch(contract_no, all_cfg, ns, opt),
}
}
Loading

0 comments on commit 399c199

Please sign in to comment.