Skip to content

Commit 399c199

Browse files
Soroban counter.sol example (#1645)
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]>
1 parent 08dbe49 commit 399c199

25 files changed

+794
-126
lines changed

.github/workflows/test.yml

+42
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,48 @@ jobs:
306306
with:
307307
name: anchor-tests
308308
path: ./target/*.profraw
309+
310+
soroban:
311+
name: Soroban Integration test
312+
runs-on: solang-ubuntu-latest
313+
container: ghcr.io/hyperledger/solang-llvm:ci-7
314+
needs: linux-x86-64
315+
steps:
316+
- name: Checkout sources
317+
uses: actions/checkout@v3
318+
- uses: actions/setup-node@v3
319+
with:
320+
node-version: '16'
321+
- uses: dtolnay/[email protected]
322+
- uses: actions/download-artifact@v3
323+
with:
324+
name: solang-linux-x86-64
325+
path: bin
326+
- name: Solang Compiler
327+
run: |
328+
chmod 755 ./bin/solang
329+
echo "$(pwd)/bin" >> $GITHUB_PATH
330+
331+
- name: Install Soroban
332+
run: cargo install --locked soroban-cli --version 21.0.0-rc.1
333+
- name: Add cargo install location to PATH
334+
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
335+
- run: npm install
336+
working-directory: ./integration/soroban
337+
- name: Build Solang contracts
338+
run: npm run build
339+
working-directory: ./integration/soroban
340+
- name: Setup Soroban enivronment
341+
run: npm run setup
342+
working-directory: ./integration/soroban
343+
- name: Deploy and test contracts
344+
run: npm run test
345+
working-directory: ./integration/soroban
346+
- name: Upload test coverage files
347+
uses: actions/[email protected]
348+
with:
349+
name: soroban-tests
350+
path: ./target/*.profraw
309351

310352
solana:
311353
name: Solana Integration test

integration/soroban/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
*.js
2+
*.so
3+
*.key
4+
*.json
5+
!tsconfig.json
6+
!package.json
7+
node_modules
8+
package-lock.json

integration/soroban/counter.sol

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
contract counter {
2+
uint64 public count = 10;
3+
4+
function increment() public returns (uint64) {
5+
count += 1;
6+
return count;
7+
}
8+
9+
function decrement() public returns (uint64) {
10+
count -= 1;
11+
return count;
12+
}
13+
}

integration/soroban/counter.spec.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as StellarSdk from '@stellar/stellar-sdk';
2+
import { readFileSync } from 'fs';
3+
import { expect } from 'chai';
4+
import path from 'path';
5+
import { fileURLToPath } from 'url';
6+
import { call_contract_function } from './test_helpers.js';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const dirname = path.dirname(__filename);
10+
11+
describe('Counter', () => {
12+
let keypair;
13+
const server = new StellarSdk.SorobanRpc.Server(
14+
"https://soroban-testnet.stellar.org:443",
15+
);
16+
17+
let contractAddr;
18+
let contract;
19+
before(async () => {
20+
21+
console.log('Setting up counter contract tests...');
22+
23+
// read secret from file
24+
const secret = readFileSync('alice.txt', 'utf8').trim();
25+
keypair = StellarSdk.Keypair.fromSecret(secret);
26+
27+
let contractIdFile = path.join(dirname, '.soroban', 'contract-ids', 'counter.txt');
28+
// read contract address from file
29+
contractAddr = readFileSync(contractIdFile, 'utf8').trim().toString();
30+
31+
// load contract
32+
contract = new StellarSdk.Contract(contractAddr);
33+
34+
// initialize the contract
35+
await call_contract_function("init", server, keypair, contract);
36+
37+
});
38+
39+
it('get correct initial counter', async () => {
40+
// get the count
41+
let count = await call_contract_function("count", server, keypair, contract);
42+
expect(count.toString()).eq("10");
43+
});
44+
45+
it('increment counter', async () => {
46+
// increment the counter
47+
await call_contract_function("increment", server, keypair, contract);
48+
49+
// get the count
50+
let count = await call_contract_function("count", server, keypair, contract);
51+
expect(count.toString()).eq("11");
52+
});
53+
});
54+
55+

integration/soroban/package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"type": "module",
3+
"dependencies": {
4+
"@stellar/stellar-sdk": "^12.0.1",
5+
"chai": "^5.1.1",
6+
"dotenv": "^16.4.5",
7+
"mocha": "^10.4.0"
8+
},
9+
"scripts": {
10+
"build": "solang compile *.sol --target soroban",
11+
"setup": "node setup.js",
12+
"test": "mocha *.spec.js --timeout 20000"
13+
},
14+
"devDependencies": {
15+
"@eslint/js": "^9.4.0",
16+
"@types/mocha": "^10.0.6",
17+
"eslint": "^9.4.0",
18+
"expect": "^29.7.0",
19+
"globals": "^15.4.0",
20+
"typescript": "^5.4.5"
21+
}
22+
}
23+

integration/soroban/setup.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
import 'dotenv/config';
3+
import { mkdirSync, readdirSync} from 'fs';
4+
import { execSync } from 'child_process';
5+
import path from 'path';
6+
import { fileURLToPath } from 'url';
7+
8+
console.log("###################### Initializing ########################");
9+
10+
// Get dirname (equivalent to the Bash version)
11+
const __filename = fileURLToPath(import.meta.url);
12+
const dirname = path.dirname(__filename);
13+
14+
// variable for later setting pinned version of soroban in "$(dirname/target/bin/soroban)"
15+
const soroban = "soroban"
16+
17+
// Function to execute and log shell commands
18+
function exe(command) {
19+
console.log(command);
20+
execSync(command, { stdio: 'inherit' });
21+
}
22+
23+
function generate_alice() {
24+
exe(`${soroban} keys generate alice --network testnet`);
25+
26+
// get the secret key of alice and put it in alice.txt
27+
exe(`${soroban} keys show alice > alice.txt`);
28+
}
29+
30+
31+
function filenameNoExtension(filename) {
32+
return path.basename(filename, path.extname(filename));
33+
}
34+
35+
function deploy(wasm) {
36+
37+
let contractId = path.join(dirname, '.soroban', 'contract-ids', filenameNoExtension(wasm) + '.txt');
38+
39+
exe(`(${soroban} contract deploy --wasm ${wasm} --ignore-checks --source-account alice --network testnet) > ${contractId}`);
40+
}
41+
42+
function deploy_all() {
43+
const contractsDir = path.join(dirname, '.soroban', 'contract-ids');
44+
mkdirSync(contractsDir, { recursive: true });
45+
46+
const wasmFiles = readdirSync(`${dirname}`).filter(file => file.endsWith('.wasm'));
47+
48+
wasmFiles.forEach(wasmFile => {
49+
deploy(path.join(dirname, wasmFile));
50+
});
51+
}
52+
53+
function add_testnet() {
54+
55+
exe(`${soroban} network add \
56+
--global testnet \
57+
--rpc-url https://soroban-testnet.stellar.org:443 \
58+
--network-passphrase "Test SDF Network ; September 2015"`);
59+
}
60+
61+
add_testnet();
62+
generate_alice();
63+
deploy_all();

integration/soroban/test_helpers.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as StellarSdk from '@stellar/stellar-sdk';
2+
3+
4+
5+
export async function call_contract_function(method, server, keypair, contract) {
6+
7+
let res;
8+
let builtTransaction = new StellarSdk.TransactionBuilder(await server.getAccount(keypair.publicKey()), {
9+
fee: StellarSdk.BASE_FEE,
10+
networkPassphrase: StellarSdk.Networks.TESTNET,
11+
}).addOperation(contract.call(method)).setTimeout(30).build();
12+
13+
let preparedTransaction = await server.prepareTransaction(builtTransaction);
14+
15+
// Sign the transaction with the source account's keypair.
16+
preparedTransaction.sign(keypair);
17+
18+
try {
19+
let sendResponse = await server.sendTransaction(preparedTransaction);
20+
if (sendResponse.status === "PENDING") {
21+
let getResponse = await server.getTransaction(sendResponse.hash);
22+
// Poll `getTransaction` until the status is not "NOT_FOUND"
23+
while (getResponse.status === "NOT_FOUND") {
24+
console.log("Waiting for transaction confirmation...");
25+
// See if the transaction is complete
26+
getResponse = await server.getTransaction(sendResponse.hash);
27+
// Wait one second
28+
await new Promise((resolve) => setTimeout(resolve, 1000));
29+
}
30+
31+
if (getResponse.status === "SUCCESS") {
32+
// Make sure the transaction's resultMetaXDR is not empty
33+
if (!getResponse.resultMetaXdr) {
34+
throw "Empty resultMetaXDR in getTransaction response";
35+
}
36+
// Find the return value from the contract and return it
37+
let transactionMeta = getResponse.resultMetaXdr;
38+
let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
39+
console.log(`Transaction result: ${returnValue.value()}`);
40+
res = returnValue.value();
41+
} else {
42+
throw `Transaction failed: ${getResponse.resultXdr}`;
43+
}
44+
} else {
45+
throw sendResponse.errorResultXdr;
46+
}
47+
} catch (err) {
48+
// Catch and report any errors we've thrown
49+
console.log("Sending transaction failed");
50+
console.log(err);
51+
}
52+
return res;
53+
}

src/bin/cli/mod.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ pub struct CompilePackage {
321321
#[arg(name = "VERSION", help = "specify contracts version", long = "version", num_args = 1, value_parser = ValueParser::new(parse_version))]
322322
#[serde(default, deserialize_with = "deserialize_version")]
323323
pub version: Option<String>,
324+
325+
#[arg(
326+
name = "SOROBAN-VERSION",
327+
help = "specify soroban contracts pre-release number",
328+
short = 's',
329+
long = "soroban-version",
330+
num_args = 1
331+
)]
332+
pub soroban_version: Option<u64>,
324333
}
325334

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

548-
pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Options {
557+
pub fn options_arg(
558+
debug: &DebugFeatures,
559+
optimizations: &Optimizations,
560+
compiler_inputs: &CompilePackage,
561+
) -> Options {
549562
let opt_level = if let Some(level) = &optimizations.opt_level {
550563
match level.as_str() {
551564
"none" => OptimizationLevel::None,
@@ -574,6 +587,7 @@ pub fn options_arg(debug: &DebugFeatures, optimizations: &Optimizations) -> Opti
574587
} else {
575588
None
576589
}),
590+
soroban_version: compiler_inputs.soroban_version,
577591
}
578592
}
579593

src/bin/cli/test.rs

+15-3
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,17 @@ mod tests {
101101

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

104-
let opt = options_arg(&default_debug, &default_optimize);
104+
let compiler_package = cli::CompilePackage {
105+
input: Some(vec![PathBuf::from("flipper.sol")]),
106+
contracts: Some(vec!["flipper".to_owned()]),
107+
import_path: Some(vec![]),
108+
import_map: Some(vec![]),
109+
authors: None,
110+
version: Some("0.1.0".to_string()),
111+
soroban_version: None,
112+
};
113+
114+
let opt = options_arg(&default_debug, &default_optimize, &compiler_package);
105115

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

@@ -185,7 +195,8 @@ mod tests {
185195
import_path: Some(vec![]),
186196
import_map: Some(vec![]),
187197
authors: None,
188-
version: Some("0.1.0".to_string())
198+
version: Some("0.1.0".to_string()),
199+
soroban_version: None
189200
},
190201
compiler_output: cli::CompilerOutput {
191202
emit: None,
@@ -239,7 +250,8 @@ mod tests {
239250
import_path: Some(vec![]),
240251
import_map: Some(vec![]),
241252
authors: Some(vec!["not_sesa".to_owned()]),
242-
version: Some("0.1.0".to_string())
253+
version: Some("0.1.0".to_string()),
254+
soroban_version: None
243255
},
244256
compiler_output: cli::CompilerOutput {
245257
emit: None,

src/bin/solang.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,13 @@ fn compile(compile_args: &Compile) {
171171

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

174-
let opt = options_arg(&compile_args.debug_features, &compile_args.optimizations);
174+
let compile_package = &compile_args.package;
175+
176+
let opt = options_arg(
177+
&compile_args.debug_features,
178+
&compile_args.optimizations,
179+
compile_package,
180+
);
175181

176182
let mut namespaces = Vec::new();
177183

src/codegen/dispatch/mod.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use crate::{sema::ast::Namespace, Target};
55

66
pub(crate) mod polkadot;
77
pub(super) mod solana;
8+
pub(super) mod soroban;
89

910
pub(super) fn function_dispatch(
1011
contract_no: usize,
11-
all_cfg: &[ControlFlowGraph],
12+
all_cfg: &mut [ControlFlowGraph],
1213
ns: &mut Namespace,
1314
opt: &Options,
1415
) -> Vec<ControlFlowGraph> {
@@ -17,6 +18,6 @@ pub(super) fn function_dispatch(
1718
Target::Polkadot { .. } | Target::EVM => {
1819
polkadot::function_dispatch(contract_no, all_cfg, ns, opt)
1920
}
20-
Target::Soroban => vec![],
21+
Target::Soroban => soroban::function_dispatch(contract_no, all_cfg, ns, opt),
2122
}
2223
}

0 commit comments

Comments
 (0)