浏览代码

soroban(fix): try fix CI tests (#1839)

Migrated test setup to use Stellar JS SDK instead of calling the CLI

---------

Signed-off-by: salaheldinsoliman <salaheldin_sameh@aucegypt.edu>
salaheldinsoliman 1 月之前
父节点
当前提交
af3142447c
共有 5 个文件被更改,包括 286 次插入95 次删除
  1. 71 51
      .github/workflows/test.yml
  2. 1 1
      Cargo.toml
  3. 1 1
      Dockerfile
  4. 1 1
      integration/soroban/auth_framework.spec.js
  5. 212 41
      integration/soroban/setup.js

+ 71 - 51
.github/workflows/test.yml

@@ -51,7 +51,7 @@ jobs:
     - name: Install Rust
       uses: dtolnay/rust-toolchain@master
       with:
-        toolchain: 1.85.0
+        toolchain: 1.87.0
         components: |
           llvm-tools
           clippy
@@ -108,7 +108,7 @@ jobs:
       run: |
         sudo apt-get update
         sudo apt-get install -y gcc g++ make
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
     - name: Get LLVM
       run: curl -sSL --output llvm16.0-linux-arm64.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-linux-arm64.tar.xz
     - name: Extract LLVM
@@ -141,7 +141,7 @@ jobs:
     # Use C:\ as D:\ might run out of space
     - name: "Use C: for rust temporary files"
       run: echo "CARGO_TARGET_DIR=C:\target" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
       with:
         components: clippy
     # We run clippy on Linux in the lint job above, but this does not check #[cfg(windows)] items
@@ -167,7 +167,7 @@ jobs:
       uses: actions/checkout@v4
       with:
         submodules: recursive
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
     - name: Get LLVM
       run: curl -sSL --output llvm16.0-mac-arm.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-mac-arm.tar.xz
     - name: Extract LLVM
@@ -193,7 +193,7 @@ jobs:
       uses: actions/checkout@v4
       with:
         submodules: recursive
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
     - name: Get LLVM
       run: wget -q -O llvm16.0-mac-intel.tar.xz https://github.com/hyperledger-solang/solang-llvm/releases/download/llvm16-0/llvm16.0-mac-intel.tar.xz
     - name: Extract LLVM
@@ -255,7 +255,7 @@ jobs:
     - uses: actions/setup-node@v4
       with:
         node-version: '16'
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
     - name: Setup yarn
       run: npm install -g yarn
     - uses: actions/download-artifact@v4.1.8
@@ -299,51 +299,71 @@ jobs:
     name: Soroban Integration test
     runs-on: solang-ubuntu-latest
     container: ghcr.io/hyperledger/solang-llvm:ci-7
+    env:
+      SSL_CERT_FILE: /etc/ssl/certs/ca-certificates.crt   # helps Node/Axios find the bundle
     needs: linux-x86-64
     steps:
-    - name: Checkout sources
-      uses: actions/checkout@v4
-    - uses: actions/setup-node@v4
-      with:
-        node-version: '16'
-    - uses: dtolnay/rust-toolchain@1.85.0
-      with:
-        target: wasm32-unknown-unknown
-    - uses: actions/download-artifact@v4.1.8
-      with:
-        name: solang-linux-x86-64
-        path: bin
-    - name: Solang Compiler
-      run: |
-        chmod 755 ./bin/solang
-        echo "$(pwd)/bin" >> $GITHUB_PATH
-    - name: Install system dependencies for stellar-cli
-      run: apt-get update && apt-get install -y libdbus-1-dev pkg-config
-    - name: Install Soroban
-      run: cargo install --locked stellar-cli --version 23.0.0
-    - name: Add wasm32v1-none target
-      run: rustup target add wasm32v1-none
-    - 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: Build rust contracts
-      run: stellar contract build --profile release-with-logs
-      working-directory: ./integration/soroban/rust/contracts
-    - 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/upload-artifact@v4.4.0
-      with:
-        name: soroban-tests
-        path: ./target/*.profraw
+      - name: Checkout sources
+        uses: actions/checkout@v4
+
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '18'   # strongly recommended; 16 is EOL (works too, but less robust)
+
+      - uses: dtolnay/rust-toolchain@1.87.0
+        with:
+          target: wasm32-unknown-unknown
+
+      - uses: actions/download-artifact@v4.1.8
+        with:
+          name: solang-linux-x86-64
+          path: bin
+
+      - name: Solang Compiler
+        run: |
+          chmod 755 ./bin/solang
+          echo "$(pwd)/bin" >> $GITHUB_PATH
+
+      - name: Install system dependencies (+ CA bundle)
+        run: |
+          apt-get update
+          apt-get install -y --no-install-recommends \
+            ca-certificates curl openssl libdbus-1-dev pkg-config
+          update-ca-certificates
+
+      - name: Install Soroban
+        run: cargo install --locked stellar-cli --version 23.0.0
+
+      - name: Add wasm32v1-none target
+        run: rustup target add wasm32v1-none
+
+      - 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: Build rust contracts
+        run: stellar contract build --profile release-with-logs
+        working-directory: ./integration/soroban/rust/contracts
+
+      - name: Setup Soroban environment
+        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/upload-artifact@v4.4.0
+        with:
+          name: soroban-tests
+          path: ./target/*.profraw
 
   solana:
     name: Solana Integration test
@@ -356,7 +376,7 @@ jobs:
     - uses: actions/setup-node@v4
       with:
         node-version: '16'
-    - uses: dtolnay/rust-toolchain@1.85.0
+    - uses: dtolnay/rust-toolchain@1.87.0
     - uses: actions/download-artifact@v4.1.8
       with:
         name: solang-linux-x86-64
@@ -533,7 +553,7 @@ jobs:
       - name: Install Rust
         uses: dtolnay/rust-toolchain@master
         with:
-          toolchain: 1.85.0
+          toolchain: 1.87.0
           components: llvm-tools
       - name: Install cargo-llvm-cov
         uses: taiki-e/install-action@cargo-llvm-cov

+ 1 - 1
Cargo.toml

@@ -8,7 +8,7 @@ license = "Apache-2.0"
 build = "build.rs"
 description = "Solang Solidity Compiler"
 keywords = [ "solidity", "compiler", "solana", "polkadot", "substrate" ]
-rust-version = "1.85.0"
+rust-version = "1.87.0"
 edition = "2021"
 exclude = [ "/.*", "/docs", "/solana-library", "/tests", "/integration", "/vscode", "/testdata" ]
 

+ 1 - 1
Dockerfile

@@ -4,7 +4,7 @@ COPY . src
 WORKDIR /src/stdlib/
 RUN make
 
-RUN rustup default 1.85.0
+RUN rustup default 1.87.0
 
 WORKDIR /src
 RUN cargo build --release

+ 1 - 1
integration/soroban/auth_framework.spec.js

@@ -49,7 +49,7 @@ describe('Auth Framework', () => {
     expect(
       res.error || toSafeJson(res),
       'Missing expected Soroban auth error message'
-    ).to.include("recording authorization only] encountered authorization not tied to the root contract invocation for an address. Use `require_auth()` in the top invocation or enable non-root authorization.");
+    ).to.include("[recording authorization only] encountered unauthorized call for a contract earlier in the call stack, make sure that you have called `authorize_as_current_contract()");
   });
 
 });

+ 212 - 41
integration/soroban/setup.js

@@ -1,69 +1,240 @@
 
 import 'dotenv/config';
-import { mkdirSync, readdirSync} from 'fs';
-import { execSync } from 'child_process';
+import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
 import path from 'path';
 import { fileURLToPath } from 'url';
+import crypto from 'crypto';
+import {
+  Keypair,
+  Address,
+  TransactionBuilder,
+  BASE_FEE,
+  Networks,
+  Operation,
+  rpc,
+  xdr,
+  StrKey,
+} from '@stellar/stellar-sdk';
+
+console.log('###################### Initializing (SDK) ########################');
 
-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"
+// --- Network config (mirrors your CLI) ---
+const RPC_URL = process.env.SOROBAN_RPC_URL || 'https://soroban-testnet.stellar.org';
+const NETWORK_PASSPHRASE = Networks.TESTNET;
 
-// Function to execute and log shell commands
-function exe(command) {
-  console.log(command);
-  execSync(command, { stdio: 'inherit' });
-}
+// --- Paths ---
+const CONTRACT_IDS_DIR = path.join(dirname, '.stellar', 'contract-ids');
+const ALICE_FILE = path.join(dirname, 'alice.txt'); // tests expect seed-only here
+
+// --- SDK server ---
+const server = new rpc.Server(RPC_URL);
 
-function generate_alice() {
-  exe(`stellar keys generate alice --network testnet --overwrite --fund`);
+// ---------- helpers ----------
+const filenameNoExtension = (filename) => path.basename(filename, path.extname(filename));
 
-  // get the secret key of alice and put it in alice.txt
-  exe(`stellar keys show alice > alice.txt`);
+function logStep(s) {
+  console.log(`\n=== ${s} ===`);
 }
 
+// Extract a valid Ed25519 seed ("S..." StrKey) from any string; return null if not found
+function extractSeed(raw) {
+  if (!raw) return null;
+  const text = String(raw).trim();
+
+  // 1) Common "secret: S..." format
+  const line = text.match(/^secret:\s*(\S+)/mi)?.[1];
+  if (line && line.startsWith('S')) return line;
+
+  // 2) Look for any S... seed inside the text (base32 chars, total length 56)
+  const m = text.match(/\bS[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]{55}\b/);
+  if (m) return m[0];
 
-function filenameNoExtension(filename) {
-  return path.basename(filename, path.extname(filename));
+  // 3) Maybe the whole file/env is just the seed
+  if (text.startsWith('S') && text.length >= 56) return text.split(/\s+/)[0];
+
+  return null;
+}
+
+// Save alice in the legacy format expected by your tests: ONLY the secret seed.
+function saveAliceTxtSeedOnly(kp) {
+  writeFileSync(ALICE_FILE, kp.secret().trim() + '\n');
 }
 
-function deploy(wasm) {
+// create/fund or reuse an account named "alice"
+async function getAlice() {
+  // prefer env override if you want to reuse a key (optional)
+  const envRaw = process.env.ALICE_SECRET?.trim();
+  if (envRaw) {
+    const seed = extractSeed(envRaw);
+    if (!seed) throw new Error('ALICE_SECRET is set but not a valid S… seed');
+    const kp = Keypair.fromSecret(seed);
+    await server.requestAirdrop(kp.publicKey()).catch(() => {}); // no-op if already funded
+    saveAliceTxtSeedOnly(kp); // normalize file for tests
+    return kp;
+  }
+
+  // if we already wrote alice.txt, parse/normalize it (supports multi-line legacy)
+  if (existsSync(ALICE_FILE)) {
+    const raw = readFileSync(ALICE_FILE, 'utf8');
+    const seed = extractSeed(raw);
+    if (seed) {
+      const kp = Keypair.fromSecret(seed);
+      await server.requestAirdrop(kp.publicKey()).catch(() => {});
+      // normalize file to seed-only so future runs & tests are stable
+      saveAliceTxtSeedOnly(kp);
+      return kp;
+    }
+    // fall through if file was malformed
+  }
+
+  // otherwise generate & fund
+  const kp = Keypair.random();
+  logStep(`Funding ${kp.publicKey()} via Friendbot`);
+  await server.requestAirdrop(kp.publicKey());
+  saveAliceTxtSeedOnly(kp);
+  return kp;
+}
 
-  let contractId = path.join(dirname, '.stellar', 'contract-ids', filenameNoExtension(wasm) + '.txt');
+async function loadSourceAccount(publicKey) {
+  // For Soroban you fetch sequence via RPC:
+  return server.getAccount(publicKey);
+}
 
-  exe(`(stellar contract deploy --wasm ${wasm} --ignore-checks --source-account alice --network testnet) > ${contractId}`);
+// Upload a WASM module (on-chain code). We also compute its SHA-256 (wasmHash) locally.
+async function uploadWasm(sourceAccount, signer, wasmBytes) {
+  const tx = new TransactionBuilder(sourceAccount, {
+    fee: BASE_FEE,
+    networkPassphrase: NETWORK_PASSPHRASE,
+  })
+    .addOperation(Operation.uploadContractWasm({ wasm: wasmBytes }))
+    .setTimeout(60)
+    .build();
+
+  // prepare (simulate adds resources/footprint), sign, send
+  const prepared = await server.prepareTransaction(tx);
+  prepared.sign(signer);
+  const sent = await server.sendTransaction(prepared);
+  await server.pollTransaction(sent.hash);
+
+  // The wasmHash is the SHA-256 of the bytes; createContract expects this hash.
+  const wasmHash = crypto.createHash('sha256').update(wasmBytes).digest(); // Buffer(32)
+  return wasmHash;
 }
 
-function deploy_all() {
-  const contractsDir = path.join(dirname, '.stellar', 'contract-ids');
-  mkdirSync(contractsDir, { recursive: true });
+// Extract the simulation return value (ScVal), supporting both parsed and base64 shapes
+function extractSimRetval(sim) {
+  const candidate = sim?.result?.retval ?? sim?.results?.[0]?.retval;
+  if (!candidate) return null;
+
+  // Parsed object (xdr.ScVal): has a .switch() function (and often .toXDR())
+  if (candidate && typeof candidate.switch === 'function') return candidate;
 
-  let wasmFiles = readdirSync(`${dirname}`).filter(file => file.endsWith('.wasm'));
-  console.log(dirname);
-  
-  let rust_wasm = path.join('rust','target','wasm32v1-none', 'release-with-logs', 'hello_world.wasm');
+  // Base64-encoded XDR string (older shapes)
+  if (typeof candidate === 'string') return xdr.ScVal.fromXDR(candidate, 'base64');
 
-  // add rust wasm file to the list of wasm files
-  wasmFiles.push(rust_wasm);
+  // xdr object with toXDR method (rare edge)
+  if (candidate && typeof candidate.toXDR === 'function') return candidate;
 
-  wasmFiles.forEach(wasmFile => {
-    deploy(path.join(dirname, wasmFile));
-  });
+  return null;
 }
 
-function add_testnet() {
+// Create a contract instance from the uploaded wasmHash.
+// Returns the "C..." contract id using simulation (no event parsing).
+async function createContract(sourceAccount, signer, wasmHash) {
+  const deployer = new Address(signer.publicKey());
+  const salt = crypto.randomBytes(32); // deterministic ID for this deployer+salt
+
+  // Build the tx (not prepared yet)
+  let createTx = new TransactionBuilder(sourceAccount, {
+    fee: BASE_FEE,
+    networkPassphrase: NETWORK_PASSPHRASE,
+  })
+    .addOperation(
+      Operation.createCustomContract({
+        address: deployer,
+        wasmHash,            // sha256(wasm bytes)
+        constructorArgs: [], // add args here if your contract has an init
+        salt,                // deterministic contract id
+      })
+    )
+    .setTimeout(60)
+    .build();
+
+  // 1) SIMULATE to read the return value (contract address) before submitting
+  const sim = await server.simulateTransaction(createTx);
+  const scv = extractSimRetval(sim);
+  if (!scv) {
+    throw new Error(
+      `simulateTransaction returned no retval for createCustomContract: ${JSON.stringify(sim)}`
+    );
+  }
+  if (scv.switch() !== xdr.ScValType.scvAddress()) {
+    throw new Error('createCustomContract retval is not an Address ScVal');
+  }
+  const scAddr = scv.address();
+  if (scAddr.switch() !== xdr.ScAddressType.scAddressTypeContract()) {
+    throw new Error('createCustomContract retval Address is not a contract');
+  }
+  const contractId = StrKey.encodeContract(scAddr.contractId()); // => "C..."
+
+  // 2) Prepare, sign, send, poll
+  createTx = await server.prepareTransaction(createTx);
+  createTx.sign(signer);
+  const sent = await server.sendTransaction(createTx);
+  await server.pollTransaction(sent.hash);
+
+  return contractId;
+}
+
+async function deployOne(wasmPath, signer) {
+  const name = filenameNoExtension(wasmPath);
+  const outFile = path.join(CONTRACT_IDS_DIR, `${name}.txt`);
+  const wasmBytes = readFileSync(wasmPath);
+
+  logStep(`Uploading WASM: ${wasmPath}`);
+  let account = await loadSourceAccount(signer.publicKey());
+  const wasmHash = await uploadWasm(account, signer, wasmBytes);
 
-  exe(`stellar network add \
-    --global testnet \
-    --rpc-url https://soroban-testnet.stellar.org:443 \
-    --network-passphrase "Test SDF Network ; September 2015"`);
+  logStep(`Creating contract for: ${name}`);
+  account = await loadSourceAccount(signer.publicKey()); // refresh sequence
+  const contractId = await createContract(account, signer, wasmHash);
+
+  mkdirSync(CONTRACT_IDS_DIR, { recursive: true });
+  writeFileSync(outFile, contractId + '\n');
+  console.log(`✔ Wrote contract id -> ${outFile}`);
+}
+
+async function deployAll() {
+  const signer = await getAlice();
+  const files = readdirSync(dirname).filter((f) => f.endsWith('.wasm'));
+
+  // include your Rust artifact, same path you used before
+  const rustWasm = path.join(
+    'rust',
+    'target',
+    'wasm32v1-none',
+    'release-with-logs',
+    'hello_world.wasm'
+  );
+  if (!files.includes(rustWasm)) files.push(rustWasm);
+
+  console.log('Found WASM files:', files);
+  for (const f of files) {
+    const full = path.join(dirname, f);
+    await deployOne(full, signer);
+  }
 }
 
-add_testnet();
-generate_alice();
-deploy_all();
+(async function main() {
+  logStep('Network');
+  console.log('RPC:', RPC_URL);
+  console.log('Passphrase:', NETWORK_PASSPHRASE);
+
+  await deployAll();
+})().catch((e) => {
+  console.error('\nDeployment failed:', e?.response ?? e);
+  process.exit(1);
+});