From 4222ef115ddc8e9ff8c1ba6b4dcf7caaaeb15c6f Mon Sep 17 00:00:00 2001 From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:56:09 +0200 Subject: [PATCH 1/5] Add multisig deploy --- .github/workflows/release-rc.yml | 29 +- .github/workflows/release.yml | 23 +- Cargo.toml | 2 +- bee_wallet/assets/multisig/Multisig.abi.json | 200 ++++++++++ bee_wallet/assets/multisig/Multisig.tvc | Bin 0 -> 5326 bytes bee_wallet/assets/multisig/PROVENANCE.md | 19 + bee_wallet/src/adapters/wasm/dto/mod.rs | 1 + bee_wallet/src/adapters/wasm/dto/multisig.rs | 48 +++ bee_wallet/src/adapters/wasm/mod.rs | 50 +++ bee_wallet/src/lib.rs | 9 + bee_wallet/src/services/mod.rs | 1 + bee_wallet/src/services/multisig.rs | 363 +++++++++++++++++++ bee_wallet/tests/integration.rs | 136 +++++++ examples/javascript/miner-react/bun.lock | 1 + examples/javascript/miner-react/src/App.tsx | 58 +-- 15 files changed, 878 insertions(+), 62 deletions(-) create mode 100644 bee_wallet/assets/multisig/Multisig.abi.json create mode 100644 bee_wallet/assets/multisig/Multisig.tvc create mode 100644 bee_wallet/assets/multisig/PROVENANCE.md create mode 100644 bee_wallet/src/adapters/wasm/dto/multisig.rs create mode 100644 bee_wallet/src/services/multisig.rs diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml index 75706a0..eba20cc 100644 --- a/.github/workflows/release-rc.yml +++ b/.github/workflows/release-rc.yml @@ -3,13 +3,8 @@ name: Release RC on: workflow_dispatch: inputs: - publish_npm: - description: "Publish RC to npm with dist-tag next" - required: true - default: true - type: boolean dry_run: - description: "Run checks and build only (no tag, no release, no npm publish)" + description: "Run checks and build only (no tag, no GitHub prerelease)" required: true default: false type: boolean @@ -60,7 +55,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" registry-url: "https://registry.npmjs.org" - name: Cache Rust @@ -135,23 +130,9 @@ jobs: exit 1 fi - - name: Validate npm token presence - if: ${{ inputs.publish_npm && !inputs.dry_run }} - shell: bash - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -z "${NPM_TOKEN}" ]; then - echo "NPM_TOKEN secret is required for npm publish" - exit 1 - fi - - - name: Publish npm package (next) - if: ${{ inputs.publish_npm && !inputs.dry_run }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npm publish ./bee_sdk/pkg --tag next --access public - + # RC does NOT publish to npm (avoids the bare-version collision with the + # stable release on dist-tag latest). It only builds, checks, tags, and + # cuts a GitHub prerelease for validation. - name: Create and push git tag if: ${{ !inputs.dry_run }} run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cdf75e6..655852e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write env: CARGO_TERM_COLOR: always CARGO_TARGET_DIR: target @@ -60,9 +61,14 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" registry-url: "https://registry.npmjs.org" + # Trusted publishing (OIDC) requires npm >= 11.5.1; Node 24's bundled npm + # may be older, so upgrade explicitly. + - name: Upgrade npm for OIDC trusted publishing + run: npm install -g npm@latest + - name: Cache Rust uses: Swatinem/rust-cache@v2 @@ -128,21 +134,10 @@ jobs: exit 1 fi - - name: Validate npm token presence - if: ${{ inputs.publish_npm && !inputs.dry_run }} - shell: bash - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - if [ -z "${NPM_TOKEN}" ]; then - echo "NPM_TOKEN secret is required for npm publish" - exit 1 - fi - + # Auth via OIDC trusted publishing (configured on the npm package for this + # repo + workflow). No NPM_TOKEN needed; provenance is attached automatically. - name: Publish npm package (latest) if: ${{ inputs.publish_npm && !inputs.dry_run }} - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: npm publish ./bee_sdk/pkg --tag latest --access public - name: Create and push git tag diff --git a/Cargo.toml b/Cargo.toml index b030910..059046b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ default-members = [ ] [workspace.package] -version = "3.0.0" +version = "3.1.0" edition = "2024" license = "LicenseRef-Acki-Nacki-Node-License" license-file = "LICENSE.md" diff --git a/bee_wallet/assets/multisig/Multisig.abi.json b/bee_wallet/assets/multisig/Multisig.abi.json new file mode 100644 index 0000000..fe099b3 --- /dev/null +++ b/bee_wallet/assets/multisig/Multisig.abi.json @@ -0,0 +1,200 @@ +{ + "ABI version": 2, + "version": "2.4", + "header": ["pubkey", "time", "expire"], + "functions": [ + { + "name": "constructor", + "inputs": [ + {"name":"owners_pubkey","type":"uint256[]"}, + {"name":"owners_address","type":"address[]"}, + {"name":"reqConfirms","type":"uint8"}, + {"name":"reqConfirmsData","type":"uint8"}, + {"name":"value","type":"uint64"} + ], + "outputs": [ + ] + }, + { + "name": "setMaxCleanupOperations", + "inputs": [ + {"name":"value","type":"uint256"} + ], + "outputs": [ + ] + }, + { + "name": "sendTransaction", + "inputs": [ + {"name":"dest","type":"address"}, + {"name":"value","type":"uint128"}, + {"name":"cc","type":"map(uint32,varuint32)"}, + {"name":"bounce","type":"bool"}, + {"name":"flags","type":"uint8"}, + {"name":"payload","type":"cell"}, + {"name":"dapp_id","type":"uint256"} + ], + "outputs": [ + {"name":"value0","type":"address"} + ] + }, + { + "name": "submitTransaction", + "inputs": [ + {"name":"dest","type":"address"}, + {"name":"value","type":"uint128"}, + {"name":"cc","type":"map(uint32,varuint32)"}, + {"name":"bounce","type":"bool"}, + {"name":"flag","type":"uint8"}, + {"name":"payload","type":"cell"}, + {"name":"dapp_id","type":"uint256"} + ], + "outputs": [ + {"name":"transId","type":"uint64"} + ] + }, + { + "name": "confirmTransaction", + "inputs": [ + {"name":"transactionId","type":"uint64"} + ], + "outputs": [ + ] + }, + { + "name": "submitDataUpdate", + "inputs": [ + {"name":"owners_pubkey","type":"uint256[]"}, + {"name":"owners_address","type":"address[]"}, + {"name":"reqConfirms","type":"uint8"}, + {"name":"reqConfirmsData","type":"uint8"} + ], + "outputs": [ + {"name":"transId","type":"uint64"} + ] + }, + { + "name": "confirmDataUpdate", + "inputs": [ + {"name":"dataUpdateId","type":"uint64"} + ], + "outputs": [ + ] + }, + { + "name": "isConfirmed", + "inputs": [ + {"name":"mask","type":"uint32"}, + {"name":"index","type":"uint8"} + ], + "outputs": [ + {"name":"confirmed","type":"bool"} + ] + }, + { + "name": "getParameters", + "inputs": [ + ], + "outputs": [ + {"name":"maxQueuedTransactions","type":"uint8"}, + {"name":"maxCustodianCount","type":"uint8"}, + {"name":"expirationTime","type":"uint64"}, + {"name":"requiredTxnConfirms","type":"uint8"}, + {"name":"requiredDataConfirms","type":"uint8"} + ] + }, + { + "name": "getTransaction", + "inputs": [ + {"name":"transactionId","type":"uint64"} + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"cc","type":"map(uint32,varuint32)"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"},{"name":"dapp_id","type":"uint256"}],"name":"trans","type":"tuple"} + ] + }, + { + "name": "getUpdateData", + "inputs": [ + {"name":"updateDataId","type":"uint64"} + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"owners_pubkey","type":"uint256[]"},{"name":"owners_address","type":"address[]"},{"name":"reqConfirms","type":"uint8"},{"name":"reqConfirmsData","type":"uint8"}],"name":"data","type":"tuple"} + ] + }, + { + "name": "getTransactions", + "inputs": [ + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"cc","type":"map(uint32,varuint32)"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"},{"name":"dapp_id","type":"uint256"}],"name":"transactions","type":"tuple[]"} + ] + }, + { + "name": "getUpdateDatas", + "inputs": [ + ], + "outputs": [ + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"owners_pubkey","type":"uint256[]"},{"name":"owners_address","type":"address[]"},{"name":"reqConfirms","type":"uint8"},{"name":"reqConfirmsData","type":"uint8"}],"name":"data","type":"tuple[]"} + ] + }, + { + "name": "getTransactionIds", + "inputs": [ + ], + "outputs": [ + {"name":"ids","type":"uint64[]"} + ] + }, + { + "name": "getUpdateCodeIds", + "inputs": [ + ], + "outputs": [ + {"name":"ids","type":"uint64[]"} + ] + }, + { + "name": "getCustodians", + "inputs": [ + ], + "outputs": [ + {"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"custodians","type":"tuple[]"} + ] + }, + { + "name": "getVersion", + "inputs": [ + ], + "outputs": [ + {"name":"value0","type":"string"}, + {"name":"value1","type":"string"} + ] + } + ], + "events": [ + { + "name": "TransferAccepted", + "inputs": [ + {"name":"payload","type":"bytes"} + ], + "outputs": [ + ] + } + ], + "fields": [ + {"init":true,"name":"_pubkey","type":"uint256"}, + {"init":false,"name":"_timestamp","type":"uint64"}, + {"init":false,"name":"_constructorFlag","type":"bool"}, + {"init":false,"name":"m_ownerKey","type":"optional(uint256)"}, + {"init":false,"name":"m_ownerAddress","type":"optional(address)"}, + {"init":false,"name":"m_requestsMask","type":"uint256"}, + {"init":false,"name":"m_requestsMaskData","type":"uint256"}, + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"dest","type":"address"},{"name":"value","type":"uint128"},{"name":"cc","type":"map(uint32,varuint32)"},{"name":"sendFlags","type":"uint16"},{"name":"payload","type":"cell"},{"name":"bounce","type":"bool"},{"name":"dapp_id","type":"uint256"}],"init":false,"name":"m_transactions","type":"map(uint64,tuple)"}, + {"components":[{"name":"id","type":"uint64"},{"name":"confirmationsMask","type":"uint32"},{"name":"signsRequired","type":"uint8"},{"name":"signsReceived","type":"uint8"},{"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"name":"creator","type":"tuple"},{"name":"owners_pubkey","type":"uint256[]"},{"name":"owners_address","type":"address[]"},{"name":"reqConfirms","type":"uint8"},{"name":"reqConfirmsData","type":"uint8"}],"init":false,"name":"m_data","type":"map(uint64,tuple)"}, + {"components":[{"name":"owner_pubkey","type":"optional(uint256)"},{"name":"owner_address","type":"optional(address)"},{"name":"index","type":"uint8"}],"init":false,"name":"m_custodians","type":"map(uint256,tuple)"}, + {"init":false,"name":"m_custodianCount","type":"uint8"}, + {"init":false,"name":"m_defaultRequiredConfirmations","type":"uint8"}, + {"init":false,"name":"m_defaultRequiredConfirmationsData","type":"uint8"}, + {"init":false,"name":"_max_cleanup_operations","type":"uint256"} + ] +} diff --git a/bee_wallet/assets/multisig/Multisig.tvc b/bee_wallet/assets/multisig/Multisig.tvc new file mode 100644 index 0000000000000000000000000000000000000000..2d92163e1a8fe5410516bdff6af5605d7d962494 GIT binary patch literal 5326 zcmcgw4^R_V8h>v$5(SlrC{%5;n}AyD(SZ23=_LYM1vEeiig@9)N)IJ@i(vIy=NjQU zww+F_pferMUh$5(YNzL@cifEPIh4+vV3c6v)y{B-G`Z_mM#-Is!`Ui~i_rxC1P-r`HPpF;}_`8?=`_w$KYh}4ZqKXX|8x7_-BFY5At_THB6qn&)fpX^i`i0Q~jmj1^DeV zR`eM+l~38MvJd9<9>nHb;;nc@t8~3S{+`)Dt52^${}8XB04Ls>rggt<=rab=CWp;TF z2|cymgl)LX+w4CV*wS;oTe^>q82kYgMCDU0=nn>3+<}7wHMGF_5nWFU1ZVKAE0cl) zJ8si_r#p$R5u8m1sh{S{7cR2%$ei7_FWsi<^1ki5ioCxz_uzY62*Gn4*Shh2#Mejp z$Q;s*aHv7^9W2b7a1aw6=h22xv`^D5{HKzGaIhoOXPj&^HE3Qseiec7skY5TH@eKB zL*!%tH4Dx^9bP2+M0D1qOYUYXguNRk6kBbAO9-V9F1z2r#vhQ!->F!iw~jYLP^(lo z6O^DjgjL@tmb?XE%}&z9_WeIFb{yYlShxeS?-I2{`|18f@v}73P^0him>zkVOSmk7 z4IXlGSvK#E3f_1dJa1Gy2Vo`PlgBg^Ng09VGwuAhQe^+SJYl-tI~x*$7B#9viE>Q9 ztKJ>5Yi`%q0!gBU`!}EZi4t{0!-&qP1>H$H7hRZchzx&2|p_D2k# z+NWkIH5vl_FyCVkm}BREE9p8>aNtdmeaBou6dX1@1_vv^pkVP_zVmRUElZ*`5_LGJ z(?N-YN)EcqL3h8KMl_x1V=0}?fFztAJbx=8On3F`<+CTGw8&YyyWh2t*26Fj{I>|5 z2!Ae~4G!#uBdOVnFKp7z!*94R${>>601!Ez1fu0+%4h&R%YSvaS|qaoKzfprMpFO3 z{w{hBm%3ZWgPLU+YLxf^YD>8j?oO*Z5)KHG5FFSkK$51j92HoSPA4Zb-5-h$YZYY0 z3c`!DXtNAi$lnwy-L7c8Qvu;-@hTJWzuaF~<03O!RlwoBJG&ayUbFbvl#bFpwsEI~ z%>ZAle7k5<1ypp8l>>_ps03O^OWJ7(wZhptT3Z<>mGLwhxvU!bObnh|M=(^R1pTtd z{4FMn?PonEkJh91s4>y6ULrV~4c>#tu(#0@_7`i|(NQ?+@q5&^pLg|m-U=a4;RWuQ|!v6y8=|cI_Hd6%|Y~W>`4YJMS1?M5}Dl zj&1cjbNANOlO5IDoE!Gm)K}M4zqV;xP0h}F6rY)4$S|-vIOh}dpi&3tADp>Zv6lVw zZ=_O9cpxR*^Bd(D4=P>r+Z6&7A%T3MEKk7Th<0Zoa}XB;UtSyfS% zWmr|Va(G#V5qAn1acKvN0?Ovd8wS=-jl3(0=m9S2?ga}*!^NKIZ91+hJTEU00tDYO zZUDAQv+=S8CGT|!pz7rs!c{2~`n2QdnUBE-9WTXw+o1_VC|Y*{9qK(r@OG3aakI{Pa~MH7cHoFBn(KKg~vB*V(5 z{t{vG`zBwwUoq~P`+aJgKzLbND_yfM@g_9R;TvQsTUH^;80(%Jgy$bdC4kbF4Jn-+ zV=0}c0^ax?2Ci^Tm?~_BVTu?*6+?=ryl5kiXPIO{3*kM4FYGgA(%j9U89k<+?!i)! zLNaTOglaKcRtb7NW=J9p281SJvR;Esk(VyNZIBf^ynCnF6TSfmsH1ymEtM#tPH4(%=x(};R?%1K zb`;N^z-dCQLg=r-j*UOtijwO{jbO-FzA|G8PHj2xV(MggO+yod$d1m_Qfh@#P)v)c ug>Ix9Xdx}2`81d2Fid+JN3PlSK_m!6y+POpg*Y5S@`u$NHR}%sOaBXj8!4Iq literal 0 HcmV?d00001 diff --git a/bee_wallet/assets/multisig/PROVENANCE.md b/bee_wallet/assets/multisig/PROVENANCE.md new file mode 100644 index 0000000..7b9ce46 --- /dev/null +++ b/bee_wallet/assets/multisig/PROVENANCE.md @@ -0,0 +1,19 @@ +# Multisig assets — provenance + +This is the **DexDo** multisig: `Multisig.abi.json` + `Multisig.tvc` are the flat +Multisig that DexDo uses, sourced from the DexDo repo: + + https://github.com/gosh-sh/dexdo → contracts/wallet/multisig/Multisig.sol + +The repo ships only the `.sol` + `.abi.json` (no `.tvc`); the TVC is compiled on +demand. Rebuild it with the same compiler the project uses: + + sold --tvm-version gosh Multisig.sol # sold 0.79.3, output is deterministic + +Then copy `Multisig.tvc` + `Multisig.abi.json` here. + +Note: this TVC differs from `ackinacki-kit`'s `contracts/abi/multisig/Multisig.tvc` +(identical ABI, different compiled code → different code_hash). We deploy +DexDo's build so addresses/code match what DexDo expects. + +Current `Multisig.tvc` sha256: d3b38bcac8f60c1274f6099fc1e75746c02a2ff22af4efc689a754fd087a86fb diff --git a/bee_wallet/src/adapters/wasm/dto/mod.rs b/bee_wallet/src/adapters/wasm/dto/mod.rs index 09b3972..dc61b79 100644 --- a/bee_wallet/src/adapters/wasm/dto/mod.rs +++ b/bee_wallet/src/adapters/wasm/dto/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod deploy; pub(crate) mod keys; pub(crate) mod miner; pub(crate) mod multifactor; +pub(crate) mod multisig; pub(crate) mod names; pub(crate) mod tx; pub(crate) mod write; diff --git a/bee_wallet/src/adapters/wasm/dto/multisig.rs b/bee_wallet/src/adapters/wasm/dto/multisig.rs new file mode 100644 index 0000000..05ebf68 --- /dev/null +++ b/bee_wallet/src/adapters/wasm/dto/multisig.rs @@ -0,0 +1,48 @@ +//! TS-typed wasm DTOs for the flat-Multisig giver deploy. `TKeyPair` is +//! declared once in `dto/mod.rs` and reused here. + +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +export type TParamsOfDeployMultisigViaGiver = { + endpoints: string[]; + keys?: TKeyPair; + owners_pubkey?: string[]; + req_confirms?: number; + req_confirms_data?: number; + constructor_value?: string; + giver_value?: string; + giver_ecc?: Record; + wait_for_active?: boolean; +}; + +export type TResultOfDeployMultisigViaGiver = { + address: string; + public: string; + secret: string; + already_deployed: boolean; + deploy_tx?: string; +}; + +export type TParamsOfMultisigBalances = { + endpoints: string[]; + address: string; +}; +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TParamsOfDeployMultisigViaGiver")] + pub type TParamsOfDeployMultisigViaGiver; + + #[wasm_bindgen(typescript_type = "TResultOfDeployMultisigViaGiver")] + pub type TResultOfDeployMultisigViaGiver; + + #[wasm_bindgen(typescript_type = "TParamsOfMultisigBalances")] + pub type TParamsOfMultisigBalances; + + /// `{ currency_id: raw_amount_string }`, e.g. `{ "2": "10000000000" }`. + #[wasm_bindgen(typescript_type = "Record")] + pub type TMultisigBalances; +} diff --git a/bee_wallet/src/adapters/wasm/mod.rs b/bee_wallet/src/adapters/wasm/mod.rs index 03c1f21..b75283e 100644 --- a/bee_wallet/src/adapters/wasm/mod.rs +++ b/bee_wallet/src/adapters/wasm/mod.rs @@ -759,6 +759,56 @@ impl Wallet { } } +/// Free function (no `Wallet` instance, no api_url/app_id): deploy a flat +/// Multisig on shellnet, funding the future address from the default giver. +/// Always returns the owner keypair — the frontend MUST persist `secret`. +#[wasm_bindgen(js_name = deploy_multisig_via_giver)] +pub async fn deploy_multisig_via_giver( + params: dto::multisig::TParamsOfDeployMultisigViaGiver, +) -> Result { + use wasm_bindgen::JsCast; + + let params_val: JsValue = params.into(); + let params: crate::services::multisig::ParamsOfDeployMultisigViaGiver = + serde_wasm_bindgen::from_value(params_val) + .map_err(|e| JsError::new(&format!("Bad TParamsOfDeployMultisigViaGiver: {e:?}")))?; + + let result = crate::services::multisig::deploy_multisig_via_giver(params) + .await + .map_err(|e| JsError::new(&format!("deploy_multisig_via_giver failed: {e:?}")))?; + + let js = serde_wasm_bindgen::to_value(&result) + .map_err(|e| JsError::new(&format!("serialize result failed: {e:?}")))?; + Ok(js.unchecked_into::()) +} + +/// Free function: ECC balances of any account by address as +/// `{ currency_id: raw_amount_string }`. Generic — works on a flat multisig, +/// unlike the multifactor-specific balance reader. +#[wasm_bindgen(js_name = multisig_balances)] +pub async fn multisig_balances( + params: dto::multisig::TParamsOfMultisigBalances, +) -> Result { + use serde::Serialize; + use wasm_bindgen::JsCast; + + let params_val: JsValue = params.into(); + let params: crate::services::multisig::ParamsOfMultisigBalances = + serde_wasm_bindgen::from_value(params_val) + .map_err(|e| JsError::new(&format!("Bad TParamsOfMultisigBalances: {e:?}")))?; + + let balances = + crate::services::multisig::multisig_balances(params.endpoints, params.address) + .await + .map_err(|e| JsError::new(&format!("multisig_balances failed: {e:?}")))?; + + // Serialize the map as a plain JS object: { "2": "10000000000", ... }. + let js = balances + .serialize(&serde_wasm_bindgen::Serializer::json_compatible()) + .map_err(|e| JsError::new(&format!("serialize balances failed: {e:?}")))?; + Ok(js.unchecked_into::()) +} + #[cfg(not(feature = "single-wasm"))] #[wasm_bindgen(start)] pub fn start() {} diff --git a/bee_wallet/src/lib.rs b/bee_wallet/src/lib.rs index b6ae030..a09dbfa 100644 --- a/bee_wallet/src/lib.rs +++ b/bee_wallet/src/lib.rs @@ -91,6 +91,15 @@ pub use crate::services::deploy::ParamsOfPrepareDeploy; pub use crate::services::miner::ParamsOfDelMiningKey; pub use crate::services::miner::ParamsOfSetMiningKeys; pub use crate::services::multifactor::cmd::ParamsOfChangeSeedPhrase; +pub use crate::services::multisig::compute_multisig_address; +pub use crate::services::multisig::deploy_multisig; +pub use crate::services::multisig::deploy_multisig_via_giver; +pub use crate::services::multisig::multisig_balances; +pub use crate::services::multisig::DeployOutcome; +pub use crate::services::multisig::MultisigDeploySpec; +pub use crate::services::multisig::ParamsOfDeployMultisigViaGiver; +pub use crate::services::multisig::ParamsOfMultisigBalances; +pub use crate::services::multisig::ResultOfDeployMultisigViaGiver; pub use crate::services::transaction::history::ParamsOfGetHistory; pub use crate::services::zkp::ParamsOfAddZKPFactor; pub use crate::services::zkp::ZkLoginCompleteWithProverParams; diff --git a/bee_wallet/src/services/mod.rs b/bee_wallet/src/services/mod.rs index e66ba5a..98e38d8 100644 --- a/bee_wallet/src/services/mod.rs +++ b/bee_wallet/src/services/mod.rs @@ -18,6 +18,7 @@ pub mod deploy; pub mod dex; pub mod miner; pub mod multifactor; +pub mod multisig; pub mod resolvers; pub mod tokens; pub mod transaction; diff --git a/bee_wallet/src/services/multisig.rs b/bee_wallet/src/services/multisig.rs new file mode 100644 index 0000000..1035c41 --- /dev/null +++ b/bee_wallet/src/services/multisig.rs @@ -0,0 +1,363 @@ +//! Flat `Multisig` deploy helpers (kit `contracts/abi/multisig/`). +//! +//! Three composable, network-agnostic bricks plus one shellnet-only +//! convenience composition: +//! +//! 1. [`compute_multisig_address`] — derive the deterministic address of a +//! not-yet-deployed Multisig. Pure crypto, usable by anyone. +//! 2. giver top-up — fund the future address from the default shellnet giver. +//! **Shellnet only** (the giver account `0:1111…` exists nowhere else); kept +//! private, reachable solely through [`deploy_multisig_via_giver`]. +//! 3. [`deploy_multisig`] — deploy a Multisig whose address is *already funded*. +//! Network-agnostic: works on any network as long as the address has a +//! balance to pay for the deploy. Idempotent (returns early if Active). +//! +//! [`deploy_multisig_via_giver`] wires 1 → 2 → 3 for a fully client-side +//! shellnet deploy. The ABI/TVC are vendored from the kit (`assets/multisig/`) +//! because the kit's `multisig` binding intentionally leaves deploy out of +//! scope. + +use std::collections::HashMap; +use std::sync::Arc; + +use ackinacki_kit::contracts::account::Account; +use ackinacki_kit::contracts::account::AccountStatus; +use ackinacki_kit::contracts::account::ParamsOfWaitAccount; +use ackinacki_kit::contracts::giver::v3::send_currency_with_flag_from_default_giver; +use ackinacki_kit::tvm_client::abi::encode_message; +use ackinacki_kit::tvm_client::abi::Abi; +use ackinacki_kit::tvm_client::abi::CallSet; +use ackinacki_kit::tvm_client::abi::DeploySet; +use ackinacki_kit::tvm_client::abi::ParamsOfEncodeMessage; +use ackinacki_kit::tvm_client::abi::Signer; +use ackinacki_kit::tvm_client::crypto::generate_random_sign_keys; +use ackinacki_kit::tvm_client::crypto::KeyPair; +use ackinacki_kit::tvm_client::processing::process_message; +use ackinacki_kit::tvm_client::processing::ParamsOfProcessMessage; +use ackinacki_kit::tvm_client::ClientConfig; +use ackinacki_kit::tvm_client::ClientContext; +use base64::Engine; +use serde::Deserialize; +use serde::Serialize; +use serde_json::json; + +use crate::errors::AppError; +use crate::errors::AppResult; + +/// Canonical flat Multisig ABI/TVC, vendored from the kit at build time so the +/// deploy is fully self-contained (the kit doesn't expose them publicly). +const MULTISIG_ABI: &str = include_str!("../../assets/multisig/Multisig.abi.json"); +const MULTISIG_TVC: &[u8] = include_bytes!("../../assets/multisig/Multisig.tvc"); + +/// ECC currency id of SHELL — the gas/value currency on Acki Nacki. Crediting +/// it shows up as the account's base `balance`. +const SHELL_CURRENCY_ID: u32 = 2; +/// Default SHELL top-up for the future address (10 SHELL, u64 raw). +const DEFAULT_GIVER_VALUE: u64 = 10_000_000_000; +/// `sendCurrencyWithFlag` flag used to fund a *not-yet-existing* address: it +/// creates the account (Uninit) and credits the carried ECC. A plain native +/// `value` transfer (flag 1) does not bring a fresh account into existence. +const GIVER_FLAG: u8 = 16; + +/// Everything needed to deterministically encode a flat Multisig deploy. The +/// owner `keys` sign their own deploy; `owners_pubkey` are the custodians +/// (`uint256[]`, each a `0x`-prefixed hex), defaulting to the owner alone. +#[derive(Debug, Clone)] +pub struct MultisigDeploySpec { + pub keys: KeyPair, + pub owners_pubkey: Vec, + pub req_confirms: u8, + pub req_confirms_data: u8, + /// Constructor `value` arg (`uint64`), passed through as a decimal string. + pub constructor_value: String, +} + +impl MultisigDeploySpec { + /// Builds the `ParamsOfEncodeMessage` shared by address computation and the + /// actual deploy, so both derive from one source of truth. + fn encode_params(&self) -> ParamsOfEncodeMessage { + let tvc_b64 = base64::engine::general_purpose::STANDARD.encode(MULTISIG_TVC); + ParamsOfEncodeMessage { + abi: Abi::Json(MULTISIG_ABI.to_string()), + address: None, + deploy_set: Some(DeploySet { + tvc: Some(tvc_b64), + code: None, + state_init: None, + workchain_id: Some(0), + initial_data: Some(json!({ "_pubkey": format!("0x{}", self.keys.public) })), + initial_pubkey: None, + }), + call_set: Some(CallSet { + function_name: "constructor".to_string(), + header: None, + input: Some(json!({ + "owners_pubkey": self.owners_pubkey, + "owners_address": [], + "reqConfirms": self.req_confirms, + "reqConfirmsData": self.req_confirms_data, + "value": self.constructor_value, + })), + }), + signer: Signer::Keys { keys: self.keys.clone() }, + processing_try_index: None, + signature_id: None, + } + } +} + +/// A fresh account is the root of its own dApp, so its `dapp_id` equals its bare +/// account-id (lookups are dApp-scoped on `>= 1.0.0` servers). +fn dapp_id_of(address: &str) -> String { + address.trim_start_matches("0:").to_string() +} + +/// Canonical dApp-scoped address for a self-rooted account (dapp_id == account): +/// `::`. Only for values returned to callers; on-chain ops use raw `0:`. +fn canonical_address(raw: &str) -> String { + let id = raw.trim_start_matches("0:"); + format!("{id}::{id}") +} + +/// **Brick 1.** Derive the deterministic address of a not-yet-deployed flat +/// Multisig. Pure local crypto — no network round-trip beyond `ctx` setup. +pub async fn compute_multisig_address( + ctx: Arc, + spec: &MultisigDeploySpec, +) -> AppResult { + let encoded = encode_message(ctx, spec.encode_params()).await?; + Ok(encoded.address) +} + +/// Outcome of [`deploy_multisig`]. +pub struct DeployOutcome { + pub address: String, + pub already_deployed: bool, + /// Deploy transaction id; `None` when the account was already Active. + pub deploy_tx: Option, +} + +/// **Brick 3.** Deploy a flat Multisig whose address is *already funded*. +/// Network-agnostic — anyone can call this once the computed address holds a +/// balance. Idempotent: if the account is already Active, returns immediately +/// without sending a deploy message. +pub async fn deploy_multisig( + ctx: Arc, + spec: &MultisigDeploySpec, + wait_for_active: bool, +) -> AppResult { + let encode_params = spec.encode_params(); + let address = encode_message(ctx.clone(), encode_params.clone()).await?.address; + let dapp_id = dapp_id_of(&address); + + // Idempotency: already deployed -> nothing to do. + let mut account = Account::new(ctx.clone(), &address, dapp_id.clone()); + account.fetch().await?; + if account.acc_type == AccountStatus::Active { + return Ok(DeployOutcome { + address: canonical_address(&address), + already_deployed: true, + deploy_tx: None, + }); + } + + let result = process_message( + ctx.clone(), + ParamsOfProcessMessage { + message_encode_params: encode_params, + send_events: false, + dapp_id, + }, + // No progress events requested; an empty Send future satisfies the bound. + |_| async {}, + ) + .await?; + + let deploy_tx = + result.transaction.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()); + + if wait_for_active { + account + .wait(ParamsOfWaitAccount { status: AccountStatus::Active, ..Default::default() }) + .await?; + } + + Ok(DeployOutcome { address: canonical_address(&address), already_deployed: false, deploy_tx }) +} + +/// Parameters for [`deploy_multisig_via_giver`]. All amounts are strings: u64 / +/// ECC values exceed `2^53` and would lose precision as a JS `number`. +#[derive(Debug, Clone, Deserialize)] +pub struct ParamsOfDeployMultisigViaGiver { + /// GQL endpoints of the (shellnet) network. + pub endpoints: Vec, + /// Owner keypair. Generated when absent — always returned to the caller. + #[serde(default)] + pub keys: Option, + /// Custodian pubkeys (`uint256[]`, each `0x`-hex). Defaults to `[owner]`. + #[serde(default)] + pub owners_pubkey: Option>, + #[serde(default)] + pub req_confirms: Option, + #[serde(default)] + pub req_confirms_data: Option, + /// Constructor `value` arg (`uint64` as string). Default `"0"`. + #[serde(default)] + pub constructor_value: Option, + /// Native top-up of the future address (u64 as string). Default `"10000000000"`. + #[serde(default)] + pub giver_value: Option, + /// ECC top-up `{ currency_id: amount(u64-string) }`. Default `{}`. + #[serde(default)] + pub giver_ecc: Option>, + /// Wait for the deployed account to reach Active. Default `true`. + #[serde(default)] + pub wait_for_active: Option, +} + +/// Result of [`deploy_multisig_via_giver`]. +#[derive(Debug, Clone, Serialize)] +pub struct ResultOfDeployMultisigViaGiver { + pub address: String, + /// Owner pubkey (hex, no `0x`). + pub public: String, + /// Owner secret (hex) — the frontend MUST persist this. + pub secret: String, + /// `true` if the address was already Active (deploy skipped). + pub already_deployed: bool, + /// Deploy tx id, when a deploy was actually sent. + pub deploy_tx: Option, +} + +/// Builds a `ClientContext` over the given endpoints, mirroring `WalletContext` +/// (disable tvm_client's internal reconnect storm; we retry one layer up). +fn make_context(endpoints: Vec) -> AppResult> { + let mut config = ClientConfig::default(); + config.network.endpoints = Some(endpoints); + config.network.max_reconnect_timeout = 0; + let ctx = ClientContext::new(config) + .map_err(|e| AppError::from(e).with_context("failed to create tvm client"))?; + Ok(Arc::new(ctx)) +} + +/// Parses a u64-as-string amount, attributing the field name on failure. +fn parse_amount(field: &str, value: &str) -> AppResult { + value + .parse::() + .map_err(|e| AppError::new(format!("invalid {field} amount `{value}`: {e}"))) +} + +/// Fully client-side flat-Multisig deploy on shellnet: compute the address +/// (brick 1), fund it from the default giver (brick 2, **shellnet only**), then +/// deploy (brick 3). Idempotent — an already-Active address skips funding and +/// deploy. Always returns the owner keypair (generated when not supplied). +/// +/// The giver lives only on shellnet; on other networks the funding step errors +/// (no giver account). Callers are expected to gate this by network, but the +/// error is surfaced clearly regardless. +pub async fn deploy_multisig_via_giver( + params: ParamsOfDeployMultisigViaGiver, +) -> AppResult { + let ctx = make_context(params.endpoints)?; + + let keys = match params.keys { + Some(keys) => keys, + None => generate_random_sign_keys(ctx.clone())?, + }; + let owners_pubkey = params + .owners_pubkey + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| vec![format!("0x{}", keys.public)]); + + let spec = MultisigDeploySpec { + keys: keys.clone(), + owners_pubkey, + req_confirms: params.req_confirms.unwrap_or(1), + req_confirms_data: params.req_confirms_data.unwrap_or(1), + constructor_value: params.constructor_value.unwrap_or_else(|| "0".to_string()), + }; + + let address = compute_multisig_address(ctx.clone(), &spec).await?; + let dapp_id = dapp_id_of(&address); + + // Idempotency: never fund/redeploy an address that's already Active. + let mut account = Account::new(ctx.clone(), &address, dapp_id); + account.fetch().await?; + if account.acc_type == AccountStatus::Active { + return Ok(ResultOfDeployMultisigViaGiver { + address: canonical_address(&address), + public: keys.public.clone(), + secret: keys.secret.clone(), + already_deployed: true, + deploy_tx: None, + }); + } + + // Brick 2 — fund the future address from the default (shellnet) giver. + // + // The address does not exist yet. On Acki Nacki a fresh account is brought + // into existence by a flag-16 currency transfer carrying SHELL (ECC[2], the + // gas currency); a plain native `value` transfer does not create it. So the + // gas top-up goes into the ECC map under SHELL, alongside any extra ECC the + // caller asked for. `giver_value` is that SHELL amount. + let giver_value = match params.giver_value { + Some(v) => parse_amount("giver_value", &v)?, + None => DEFAULT_GIVER_VALUE, + }; + let mut giver_ecc = HashMap::new(); + giver_ecc.insert(SHELL_CURRENCY_ID, giver_value); + // Caller-specified ECC overrides/extends the default SHELL gas top-up. + for (currency, amount) in params.giver_ecc.unwrap_or_default() { + giver_ecc.insert(currency, parse_amount("giver_ecc", &amount)?); + } + + send_currency_with_flag_from_default_giver(ctx.clone(), &address, 0, giver_ecc, GIVER_FLAG) + .await + .map_err(|e| { + AppError::from(e) + .with_context("giver top-up failed (giver is available on shellnet only)") + })?; + + // Wait until the value message lands and the account exists (Uninit). + account + .wait(ParamsOfWaitAccount { status: AccountStatus::Uninit, ..Default::default() }) + .await?; + + // Brick 3 — deploy now that the address is funded. + let outcome = deploy_multisig(ctx, &spec, params.wait_for_active.unwrap_or(true)).await?; + + Ok(ResultOfDeployMultisigViaGiver { + address: outcome.address, + public: keys.public.clone(), + secret: keys.secret.clone(), + already_deployed: outcome.already_deployed, + deploy_tx: outcome.deploy_tx, + }) +} + +/// Parameters for [`multisig_balances`] (wasm boundary). +#[derive(Debug, Clone, Deserialize)] +pub struct ParamsOfMultisigBalances { + pub endpoints: Vec, + pub address: String, +} + +/// ECC balances of any account by address. dApp id == account (self-rooted): +/// canonical `::` → part before `::`; else raw `0:`. +/// +/// Generic (works on a flat multisig), unlike the multifactor-specific balance +/// reader. Returns raw integer amounts as strings keyed by ECC currency id +/// (1=NACKL, 2=SHELL, 3=USDC); the client applies per-token decimals. +pub async fn multisig_balances( + endpoints: Vec, + address: String, +) -> AppResult> { + let ctx = make_context(endpoints)?; + let dapp_id = address + .split_once("::") + .map(|(d, _)| d.to_string()) + .unwrap_or_else(|| address.trim_start_matches("0:").to_string()); + let mut account = Account::new(ctx, &address, dapp_id); + account.fetch().await?; + Ok(account.ecc.iter().map(|(k, v)| (*k, v.to_string())).collect()) +} diff --git a/bee_wallet/tests/integration.rs b/bee_wallet/tests/integration.rs index 409689f..77ad6eb 100644 --- a/bee_wallet/tests/integration.rs +++ b/bee_wallet/tests/integration.rs @@ -4297,3 +4297,139 @@ async fn test_generate_voucher_gas() { // `tests/dex_flows/flows.rs::test_production_flow_voucher_deploy_pn_and_stake`. // It exercises the same wallet.generate_voucher entry point but binds the // voucher to a real halo2 proof, which is now mandatory on RootPN. + +// --- deploy flat Multisig via default giver (shellnet, fully client-side) --- + +const SHELLNET_ENDPOINTS: &[&str] = &["shellnet.ackinacki.org"]; + +fn shellnet_endpoints() -> Vec { + SHELLNET_ENDPOINTS.iter().map(|s| s.to_string()).collect() +} + +/// End-to-end: generate owner keys, fund the future address from the giver, +/// deploy the Multisig, confirm Active on-chain. Then re-run with the SAME keys +/// and assert idempotency (no second deploy). +#[tokio::test] +async fn test_deploy_multisig_via_giver() { + let result = bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { + endpoints: shellnet_endpoints(), + keys: None, + owners_pubkey: None, + req_confirms: None, + req_confirms_data: None, + constructor_value: None, + giver_value: None, + giver_ecc: None, + wait_for_active: Some(true), + }) + .await + .expect("deploy_multisig_via_giver failed"); + + println!( + "deployed multisig: address={} public={} already_deployed={} tx={:?}", + result.address, result.public, result.already_deployed, result.deploy_tx + ); + + // Canonical dApp-scoped address: `::` with both halves equal 64-hex. + let (left, right) = + result.address.split_once("::").expect("address must be ::"); + assert_eq!(left, right, "both halves must be equal, got {}", result.address); + assert_eq!(left.len(), 64, "id half must be 64-hex, got {}", result.address); + assert!( + left.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')), + "id must be lowercase hex, got {}", + result.address + ); + assert_eq!(result.public.len(), 64, "owner public must be 64-hex"); + assert_eq!(result.secret.len(), 64, "owner secret must be 64-hex"); + assert!(!result.already_deployed, "fresh keys: should have deployed"); + assert!(result.deploy_tx.is_some(), "fresh deploy should report a tx id"); + + // Confirm the account is actually Active on-chain (reconstruct raw `0:`). + let ctx = create_tvm_context(); + let raw_address = format!("0:{left}"); + let mut account = + ackinacki_kit::contracts::account::Account::new(ctx, &raw_address, left.to_string()); + account.fetch().await.expect("fetch deployed multisig"); + assert_eq!( + account.acc_type, + ackinacki_kit::contracts::account::AccountStatus::Active, + "multisig should be Active after deploy" + ); + + // Idempotency: same keys -> same address, no second deploy, no giver spend. + let keys = KeyPair { public: result.public.clone(), secret: result.secret.clone() }; + let again = + bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { + endpoints: shellnet_endpoints(), + keys: Some(keys), + owners_pubkey: None, + req_confirms: None, + req_confirms_data: None, + constructor_value: None, + giver_value: None, + giver_ecc: None, + wait_for_active: Some(true), + }) + .await + .expect("idempotent re-deploy failed"); + + assert_eq!(again.address, result.address, "same keys must yield same address"); + assert!(again.already_deployed, "second run must detect existing Active account"); + assert!(again.deploy_tx.is_none(), "idempotent run must not deploy again"); + + // Generic ECC balance read works on the flat multisig (canonical address in). + // Note: giver SHELL lands in the account's base `balance`, so ECC[2] reads + // back as a registered-but-zero slot here; this binding returns `account.ecc` + // verbatim by design. + let balances = + bee_wallet::multisig_balances(shellnet_endpoints(), result.address.clone()) + .await + .expect("multisig_balances failed"); + println!("multisig_balances = {balances:?}"); + assert!( + balances.contains_key(&2), + "SHELL (ECC[2]) slot should be present, got {balances:?}" + ); +} + +/// Brick 1 in isolation: address derivation is deterministic for fixed inputs +/// and key-dependent. No giver / no deploy — pure encode. +#[tokio::test] +async fn test_compute_multisig_address_deterministic() { + let ctx = create_tvm_context(); + let keys = ackinacki_kit::tvm_client::crypto::generate_random_sign_keys(ctx.clone()) + .expect("generate keys"); + + let spec = bee_wallet::MultisigDeploySpec { + keys: keys.clone(), + owners_pubkey: vec![format!("0x{}", keys.public)], + req_confirms: 1, + req_confirms_data: 1, + constructor_value: "0".to_string(), + }; + + let a = bee_wallet::compute_multisig_address(ctx.clone(), &spec) + .await + .expect("compute address a"); + let b = bee_wallet::compute_multisig_address(ctx.clone(), &spec) + .await + .expect("compute address b"); + assert_eq!(a, b, "address must be deterministic for fixed spec"); + assert!(a.starts_with("0:")); + + // Different owner keys -> different address. + let other_keys = ackinacki_kit::tvm_client::crypto::generate_random_sign_keys(ctx.clone()) + .expect("generate other keys"); + let other_spec = bee_wallet::MultisigDeploySpec { + keys: other_keys.clone(), + owners_pubkey: vec![format!("0x{}", other_keys.public)], + req_confirms: 1, + req_confirms_data: 1, + constructor_value: "0".to_string(), + }; + let c = bee_wallet::compute_multisig_address(ctx, &other_spec) + .await + .expect("compute address c"); + assert_ne!(a, c, "different keys must yield a different address"); +} diff --git a/examples/javascript/miner-react/bun.lock b/examples/javascript/miner-react/bun.lock index b67a04a..5162cc7 100644 --- a/examples/javascript/miner-react/bun.lock +++ b/examples/javascript/miner-react/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "miner", diff --git a/examples/javascript/miner-react/src/App.tsx b/examples/javascript/miner-react/src/App.tsx index 41ef2f2..ddd962b 100644 --- a/examples/javascript/miner-react/src/App.tsx +++ b/examples/javascript/miner-react/src/App.tsx @@ -15,7 +15,7 @@ import "./App.css"; import { useEffect, useRef, useState } from "react"; const APP_ID = "0x0000000000000000000000000000000000000000000000000000000000000000"; -const ENDPOINTS = ["https://shellnet.ackinacki.org"]; +const ENDPOINTS = ["https://mainnet.ackinacki.org"]; const API_URL = "https://app-backend-dev.ackinacki.org/api"; const ACTIVE_SESSION_STORAGE_KEY = "bee_connect_demo_active_session_v1"; const MINING_KEYS_STORAGE_PREFIX = "bee_connect_demo_mining_keys_v1"; @@ -242,16 +242,21 @@ async function isSessionStillActive(connection: WalletConnection): Promise
- +
@@ -472,7 +473,12 @@ function VerifyWalletPanel({ 30, 1000, ); - console.log("[VERIFY] sign_challenge sent. sent_at:", challengeResult.sent_at, "message_id:", challengeResult.message_id); + console.log( + "[VERIFY] sign_challenge sent. sent_at:", + challengeResult.sent_at, + "message_id:", + challengeResult.message_id, + ); // Do NOT persist session state yet — if wait_challenge_response times out, // the DH chain stays at the pre-challenge position so retry is possible. @@ -489,12 +495,17 @@ function VerifyWalletPanel({ 120, 2000, ); - console.log("[VERIFY] challenge_response received. nonce match:", response.nonce === nonce, "wallet:", response.wallet_address); + console.log( + "[VERIFY] challenge_response received. nonce match:", + response.nonce === nonce, + "wallet:", + response.wallet_address, + ); // Persist session state only after successful response — // both sign_challenge outbound rekey and challenge_response inbound rekey. - const finalState = response.updated_session_state_json - ?? challengeResult.updated_session_state_json; + const finalState = + response.updated_session_state_json ?? challengeResult.updated_session_state_json; if (finalState) { connection.sessionStateJson = finalState; writeStoredWalletConnection(connection); @@ -1115,11 +1126,14 @@ function App() { } if (!exists) { - console.error("[SESSION_MONITOR] session inactive — dropping connection. wallet:", walletConnection.walletName, "profile:", walletConnection.profileAddress); - clearStoredMiningKeys(walletConnection); - setSessionMonitorError( - "Wallet disconnected the session. Please reconnect.", + console.error( + "[SESSION_MONITOR] session inactive — dropping connection. wallet:", + walletConnection.walletName, + "profile:", + walletConnection.profileAddress, ); + clearStoredMiningKeys(walletConnection); + setSessionMonitorError("Wallet disconnected the session. Please reconnect."); setWalletConnection(null); return; } @@ -1276,9 +1290,7 @@ function App() { (NACKL: {nacklBalance ?? "..."}) {walletConnection.inlineChallenge && ( - - verified at connect - + verified at connect )}
{walletConnection.inlineChallenge && ( From 9d44f770988a9b3f15266ea16382fea27b117b57 Mon Sep 17 00:00:00 2001 From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:20:35 +0200 Subject: [PATCH 2/5] Pin dexsdk v1.0.0 --- .github/workflows/release-rc.yml | 151 ---------------------------- .github/workflows/release.yml | 131 +++++++++++------------- bee_wallet/Cargo.toml | 4 +- bee_wallet/src/adapters/wasm/mod.rs | 7 +- bee_wallet/src/services/multisig.rs | 22 ++-- bee_wallet/tests/integration.rs | 84 +++++++--------- 6 files changed, 111 insertions(+), 288 deletions(-) delete mode 100644 .github/workflows/release-rc.yml diff --git a/.github/workflows/release-rc.yml b/.github/workflows/release-rc.yml deleted file mode 100644 index eba20cc..0000000 --- a/.github/workflows/release-rc.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Release RC - -on: - workflow_dispatch: - inputs: - dry_run: - description: "Run checks and build only (no tag, no GitHub prerelease)" - required: true - default: false - type: boolean - -concurrency: - group: release-rc-${{ github.ref }} - cancel-in-progress: false - -jobs: - release_rc: - name: Build and Publish RC - runs-on: ubuntu-latest - permissions: - contents: write - env: - CARGO_TERM_COLOR: always - CARGO_TARGET_DIR: target - CARGO_NET_GIT_FETCH_WITH_CLI: "true" - steps: - - name: Ensure workflow runs from main - run: | - if [ "${GITHUB_REF}" != "refs/heads/main" ]; then - echo "Run this workflow from main only. Current ref: ${GITHUB_REF}" - exit 1 - fi - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Rust - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - # `gosh_tls_lib` is a private dependency (bee_wallet -> tls_connect); fetch - # it over https with a read-only token. `dodex-sdk` is a dev-dependency for - # the `dex_flows` integration tests only (not built here) and pulls a heavy - # halo2 graph, so strip it. Every other git dependency is public. - - name: Configure git for private dep - env: - PAT: ${{ secrets.GH_PAT }} - run: | - git config --global url."https://oauth2:${PAT}@github.com/".insteadOf "ssh://git@github.com/" - sed -i '/^dodex-sdk = /d' bee_wallet/Cargo.toml - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "24" - registry-url: "https://registry.npmjs.org" - - - name: Cache Rust - uses: Swatinem/rust-cache@v2 - - - name: Install wasm-pack - run: cargo install wasm-pack --locked - - - name: Cargo check (workspace, native) - run: cargo check --workspace --exclude bee-sdk --exclude bee-verifier - - - name: Cargo check (verifier) - run: cargo check -p bee-verifier - - - name: Cargo check (miner wasm feature) - run: cargo check -p bee-miner --no-default-features --features wasm - - - name: Cargo clippy (workspace libs) - run: cargo clippy --workspace --exclude bee-sdk --exclude bee-verifier --lib -- -D warnings -A clippy::result_large_err - - - name: Cargo test (workspace libs) - run: cargo test --workspace --exclude bee-sdk --exclude bee-verifier --lib - - - name: Cargo test (verifier lib) - run: cargo test -p bee-verifier --lib - - - name: Cargo test (miner wasm feature on host) - run: cargo test -p bee-miner --no-default-features --features wasm --lib -- --skip core::keys::tests::gen_mining_keys_generates_keys_and_valid_deep_link - - - name: Resolve SDK version and RC tag - id: meta - shell: bash - run: | - set -euo pipefail - # bee_sdk inherits its version from [workspace.package] in the root - # Cargo.toml, so resolve it there. - VERSION="$(grep -E '^version\s*=' Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')" - if [ -z "${VERSION}" ]; then - echo "Failed to resolve workspace version from Cargo.toml" - exit 1 - fi - - LAST_RC="$(git tag -l "v${VERSION}-rc.*" | sed -E "s/^v${VERSION}-rc\.([0-9]+)$/\1/" | sort -n | tail -n1)" - if [ -z "${LAST_RC}" ]; then - NEXT_RC=1 - else - NEXT_RC=$((LAST_RC + 1)) - fi - - TAG="v${VERSION}-rc.${NEXT_RC}" - if git rev-parse "${TAG}" >/dev/null 2>&1; then - echo "Tag ${TAG} already exists" - exit 1 - fi - - echo "sdk_version=${VERSION}" >> "${GITHUB_OUTPUT}" - echo "tag=${TAG}" >> "${GITHUB_OUTPUT}" - - - name: Build wasm sdk package - run: | - cd bee_sdk - rm -rf pkg - wasm-pack build --target web --release --scope teamgosh - - - name: Validate package version - shell: bash - run: | - set -euo pipefail - PKG_VERSION="$(node -p "require('./bee_sdk/pkg/package.json').version")" - if [ "${PKG_VERSION}" != "${{ steps.meta.outputs.sdk_version }}" ]; then - echo "Version mismatch: bee_sdk/Cargo.toml=${{ steps.meta.outputs.sdk_version }}, bee_sdk/pkg/package.json=${PKG_VERSION}" - exit 1 - fi - - # RC does NOT publish to npm (avoids the bare-version collision with the - # stable release on dist-tag latest). It only builds, checks, tags, and - # cuts a GitHub prerelease for validation. - - name: Create and push git tag - if: ${{ !inputs.dry_run }} - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "${{ steps.meta.outputs.tag }}" - git push origin "${{ steps.meta.outputs.tag }}" - - - name: Create GitHub prerelease - if: ${{ !inputs.dry_run }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.meta.outputs.tag }} - name: ${{ steps.meta.outputs.tag }} - prerelease: true - generate_release_notes: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 655852e..37cff1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,18 +1,15 @@ name: Release +# Two modes, one workflow (same file = same npm trusted publisher): +# - push to main -> STABLE: publish @latest, tag vX.Y.Z, GitHub release. +# - workflow_dispatch -> TEST: publish -rc.@next (no tag, +# no release, never touches @latest). Runnable from +# ANY branch — used to validate npm publishing +# without merging to main or risking the latest tag. on: + push: + branches: [main] workflow_dispatch: - inputs: - publish_npm: - description: "Publish release to npm with dist-tag latest" - required: true - default: true - type: boolean - dry_run: - description: "Run checks and build only (no tag, no release, no npm publish)" - required: true - default: false - type: boolean concurrency: group: release-${{ github.ref }} @@ -20,23 +17,16 @@ concurrency: jobs: release: - name: Build and Publish Release + name: Build and Publish runs-on: ubuntu-latest permissions: contents: write - id-token: write + id-token: write # OIDC trusted publishing to npm env: CARGO_TERM_COLOR: always CARGO_TARGET_DIR: target CARGO_NET_GIT_FETCH_WITH_CLI: "true" steps: - - name: Ensure workflow runs from main - run: | - if [ "${GITHUB_REF}" != "refs/heads/main" ]; then - echo "Run this workflow from main only. Current ref: ${GITHUB_REF}" - exit 1 - fi - - name: Checkout uses: actions/checkout@v4 with: @@ -75,84 +65,75 @@ jobs: - name: Install wasm-pack run: cargo install wasm-pack --locked - - name: Cargo check (workspace, native) - run: cargo check --workspace --exclude bee-sdk --exclude bee-verifier - - - name: Cargo check (verifier) - run: cargo check -p bee-verifier - - - name: Cargo check (miner wasm feature) - run: cargo check -p bee-miner --no-default-features --features wasm - - - name: Cargo clippy (workspace libs) - run: cargo clippy --workspace --exclude bee-sdk --exclude bee-verifier --lib -- -D warnings -A clippy::result_large_err - - - name: Cargo test (workspace libs) - run: cargo test --workspace --exclude bee-sdk --exclude bee-verifier --lib - - - name: Cargo test (verifier lib) - run: cargo test -p bee-verifier --lib - - - name: Cargo test (miner wasm feature on host) - run: cargo test -p bee-miner --no-default-features --features wasm --lib -- --skip core::keys::tests::gen_mining_keys_generates_keys_and_valid_deep_link - - - name: Resolve SDK version and release tag + - name: Resolve version and mode id: meta shell: bash run: | set -euo pipefail - # bee_sdk inherits its version from [workspace.package] in the root - # Cargo.toml, so resolve it there. - VERSION="$(grep -E '^version\s*=' Cargo.toml | head -n1 | sed -E 's/version\s*=\s*"([^"]+)"/\1/')" + VERSION="$(grep -A1 '\[workspace\.package\]' Cargo.toml | grep 'version' | sed -E 's/.*"([^"]+)".*/\1/')" if [ -z "${VERSION}" ]; then - echo "Failed to resolve workspace version from Cargo.toml" + echo "::error::Could not parse workspace.package.version from Cargo.toml" exit 1 fi - TAG="v${VERSION}" - if git rev-parse "${TAG}" >/dev/null 2>&1; then - echo "Tag ${TAG} already exists" - exit 1 + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # Manual test publish: a unique prerelease to dist-tag `next`. + # run_number keeps each test version distinct (no collision, no UI input). + MODE="prerelease" + PKG_VERSION="${VERSION}-rc.${{ github.run_number }}" + DIST_TAG="next" + RELEASE="true" + else + # push to main: stable release to `latest`, tagged. Idempotent: if the + # tag already exists (re-push without a version bump), do nothing. + MODE="stable" + PKG_VERSION="${VERSION}" + DIST_TAG="latest" + if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then + echo "Tag ${TAG} already exists — nothing to release." + RELEASE="false" + else + RELEASE="true" + fi fi - echo "sdk_version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "mode=${MODE}" >> "${GITHUB_OUTPUT}" + echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" + echo "pkg_version=${PKG_VERSION}" >> "${GITHUB_OUTPUT}" + echo "dist_tag=${DIST_TAG}" >> "${GITHUB_OUTPUT}" echo "tag=${TAG}" >> "${GITHUB_OUTPUT}" + echo "release=${RELEASE}" >> "${GITHUB_OUTPUT}" + echo "mode=${MODE} version=${VERSION} pkg=${PKG_VERSION} -> @${DIST_TAG} release=${RELEASE}" - name: Build wasm sdk package + if: ${{ steps.meta.outputs.release == 'true' }} run: | cd bee_sdk rm -rf pkg wasm-pack build --target web --release --scope teamgosh - - name: Validate package version + - name: Set package version + if: ${{ steps.meta.outputs.release == 'true' }} shell: bash run: | - set -euo pipefail - PKG_VERSION="$(node -p "require('./bee_sdk/pkg/package.json').version")" - if [ "${PKG_VERSION}" != "${{ steps.meta.outputs.sdk_version }}" ]; then - echo "Version mismatch: bee_sdk/Cargo.toml=${{ steps.meta.outputs.sdk_version }}, bee_sdk/pkg/package.json=${PKG_VERSION}" - exit 1 - fi + cd bee_sdk/pkg + node -e "const p=require('./package.json'); p.version='${{ steps.meta.outputs.pkg_version }}'; require('fs').writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n')" + echo "pkg version set to $(node -p "require('./package.json').version")" # Auth via OIDC trusted publishing (configured on the npm package for this - # repo + workflow). No NPM_TOKEN needed; provenance is attached automatically. - - name: Publish npm package (latest) - if: ${{ inputs.publish_npm && !inputs.dry_run }} - run: npm publish ./bee_sdk/pkg --tag latest --access public + # repo + this workflow filename). No NPM_TOKEN; provenance attaches automatically. + - name: Publish to npm + if: ${{ steps.meta.outputs.release == 'true' }} + run: npm publish ./bee_sdk/pkg --tag ${{ steps.meta.outputs.dist_tag }} --access public - - name: Create and push git tag - if: ${{ !inputs.dry_run }} + - name: Tag and GitHub release (stable only) + if: ${{ steps.meta.outputs.mode == 'stable' && steps.meta.outputs.release == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "${{ steps.meta.outputs.tag }}" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.meta.outputs.tag }}" -m "Release ${{ steps.meta.outputs.tag }}" git push origin "${{ steps.meta.outputs.tag }}" - - - name: Create GitHub release - if: ${{ !inputs.dry_run }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.meta.outputs.tag }} - name: ${{ steps.meta.outputs.tag }} - prerelease: false - generate_release_notes: true + gh release create "${{ steps.meta.outputs.tag }}" --generate-notes --title "${{ steps.meta.outputs.tag }}" diff --git a/bee_wallet/Cargo.toml b/bee_wallet/Cargo.toml index e825e65..b176812 100644 --- a/bee_wallet/Cargo.toml +++ b/bee_wallet/Cargo.toml @@ -84,7 +84,7 @@ tokio = { optional = true, version = "1.49.0", features = ["rt", "macros"] } # `KitModule::External` hook. Native-only: voucher generation isn't exposed on # wasm, so this keeps the dodex graph out of the wasm build. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -dodex-contracts = { git = "https://github.com/gosh-sh/dodex-backend.git", branch = "feature/NODE-3625-move-dex-contract-wrappers-from-kit", package = "dodex-contracts" } +dodex-contracts = { git = "https://github.com/gosh-sh/dodex-backend.git", tag = "v1.0.0", package = "dodex-contracts" } [dev-dependencies] ackinacki-kit = { git = "https://github.com/gosh-sh/ackinacki-kit.git", tag = "v3.0.0", features = ["default", "contracts"] } @@ -102,7 +102,7 @@ tokio = { version = "1.49.0", features = ["rt", "macros", "time"] } # exercise bee-wallet + DEX together. Native-only: pulls the heavy halo2 graph. # Raw contract wrappers come from `dodex-contracts` (a regular dep above). [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -dodex-sdk = { git = "https://github.com/gosh-sh/dodex-backend.git", branch = "feature/NODE-3625-move-dex-contract-wrappers-from-kit" } +dodex-sdk = { git = "https://github.com/gosh-sh/dodex-backend.git", tag = "v1.0.0" } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.56" diff --git a/bee_wallet/src/adapters/wasm/mod.rs b/bee_wallet/src/adapters/wasm/mod.rs index b75283e..f370721 100644 --- a/bee_wallet/src/adapters/wasm/mod.rs +++ b/bee_wallet/src/adapters/wasm/mod.rs @@ -797,10 +797,9 @@ pub async fn multisig_balances( serde_wasm_bindgen::from_value(params_val) .map_err(|e| JsError::new(&format!("Bad TParamsOfMultisigBalances: {e:?}")))?; - let balances = - crate::services::multisig::multisig_balances(params.endpoints, params.address) - .await - .map_err(|e| JsError::new(&format!("multisig_balances failed: {e:?}")))?; + let balances = crate::services::multisig::multisig_balances(params.endpoints, params.address) + .await + .map_err(|e| JsError::new(&format!("multisig_balances failed: {e:?}")))?; // Serialize the map as a plain JS object: { "2": "10000000000", ... }. let js = balances diff --git a/bee_wallet/src/services/multisig.rs b/bee_wallet/src/services/multisig.rs index 1035c41..2509fc5 100644 --- a/bee_wallet/src/services/multisig.rs +++ b/bee_wallet/src/services/multisig.rs @@ -8,9 +8,10 @@ //! 2. giver top-up — fund the future address from the default shellnet giver. //! **Shellnet only** (the giver account `0:1111…` exists nowhere else); kept //! private, reachable solely through [`deploy_multisig_via_giver`]. -//! 3. [`deploy_multisig`] — deploy a Multisig whose address is *already funded*. -//! Network-agnostic: works on any network as long as the address has a -//! balance to pay for the deploy. Idempotent (returns early if Active). +//! 3. [`deploy_multisig`] — deploy a Multisig whose address is *already +//! funded*. Network-agnostic: works on any network as long as the address +//! has a balance to pay for the deploy. Idempotent (returns early if +//! Active). //! //! [`deploy_multisig_via_giver`] wires 1 → 2 → 3 for a fully client-side //! shellnet deploy. The ABI/TVC are vendored from the kit (`assets/multisig/`) @@ -106,14 +107,15 @@ impl MultisigDeploySpec { } } -/// A fresh account is the root of its own dApp, so its `dapp_id` equals its bare -/// account-id (lookups are dApp-scoped on `>= 1.0.0` servers). +/// A fresh account is the root of its own dApp, so its `dapp_id` equals its +/// bare account-id (lookups are dApp-scoped on `>= 1.0.0` servers). fn dapp_id_of(address: &str) -> String { address.trim_start_matches("0:").to_string() } -/// Canonical dApp-scoped address for a self-rooted account (dapp_id == account): -/// `::`. Only for values returned to callers; on-chain ops use raw `0:`. +/// Canonical dApp-scoped address for a self-rooted account (dapp_id == +/// account): `::`. Only for values returned to callers; on-chain ops +/// use raw `0:`. fn canonical_address(raw: &str) -> String { let id = raw.trim_start_matches("0:"); format!("{id}::{id}") @@ -173,8 +175,7 @@ pub async fn deploy_multisig( ) .await?; - let deploy_tx = - result.transaction.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()); + let deploy_tx = result.transaction.get("id").and_then(|v| v.as_str()).map(|s| s.to_string()); if wait_for_active { account @@ -204,7 +205,8 @@ pub struct ParamsOfDeployMultisigViaGiver { /// Constructor `value` arg (`uint64` as string). Default `"0"`. #[serde(default)] pub constructor_value: Option, - /// Native top-up of the future address (u64 as string). Default `"10000000000"`. + /// Native top-up of the future address (u64 as string). Default + /// `"10000000000"`. #[serde(default)] pub giver_value: Option, /// ECC top-up `{ currency_id: amount(u64-string) }`. Default `{}`. diff --git a/bee_wallet/tests/integration.rs b/bee_wallet/tests/integration.rs index 77ad6eb..7c5d175 100644 --- a/bee_wallet/tests/integration.rs +++ b/bee_wallet/tests/integration.rs @@ -4311,19 +4311,20 @@ fn shellnet_endpoints() -> Vec { /// and assert idempotency (no second deploy). #[tokio::test] async fn test_deploy_multisig_via_giver() { - let result = bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { - endpoints: shellnet_endpoints(), - keys: None, - owners_pubkey: None, - req_confirms: None, - req_confirms_data: None, - constructor_value: None, - giver_value: None, - giver_ecc: None, - wait_for_active: Some(true), - }) - .await - .expect("deploy_multisig_via_giver failed"); + let result = + bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { + endpoints: shellnet_endpoints(), + keys: None, + owners_pubkey: None, + req_confirms: None, + req_confirms_data: None, + constructor_value: None, + giver_value: None, + giver_ecc: None, + wait_for_active: Some(true), + }) + .await + .expect("deploy_multisig_via_giver failed"); println!( "deployed multisig: address={} public={} already_deployed={} tx={:?}", @@ -4331,8 +4332,7 @@ async fn test_deploy_multisig_via_giver() { ); // Canonical dApp-scoped address: `::` with both halves equal 64-hex. - let (left, right) = - result.address.split_once("::").expect("address must be ::"); + let (left, right) = result.address.split_once("::").expect("address must be ::"); assert_eq!(left, right, "both halves must be equal, got {}", result.address); assert_eq!(left.len(), 64, "id half must be 64-hex, got {}", result.address); assert!( @@ -4359,20 +4359,19 @@ async fn test_deploy_multisig_via_giver() { // Idempotency: same keys -> same address, no second deploy, no giver spend. let keys = KeyPair { public: result.public.clone(), secret: result.secret.clone() }; - let again = - bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { - endpoints: shellnet_endpoints(), - keys: Some(keys), - owners_pubkey: None, - req_confirms: None, - req_confirms_data: None, - constructor_value: None, - giver_value: None, - giver_ecc: None, - wait_for_active: Some(true), - }) - .await - .expect("idempotent re-deploy failed"); + let again = bee_wallet::deploy_multisig_via_giver(bee_wallet::ParamsOfDeployMultisigViaGiver { + endpoints: shellnet_endpoints(), + keys: Some(keys), + owners_pubkey: None, + req_confirms: None, + req_confirms_data: None, + constructor_value: None, + giver_value: None, + giver_ecc: None, + wait_for_active: Some(true), + }) + .await + .expect("idempotent re-deploy failed"); assert_eq!(again.address, result.address, "same keys must yield same address"); assert!(again.already_deployed, "second run must detect existing Active account"); @@ -4382,15 +4381,11 @@ async fn test_deploy_multisig_via_giver() { // Note: giver SHELL lands in the account's base `balance`, so ECC[2] reads // back as a registered-but-zero slot here; this binding returns `account.ecc` // verbatim by design. - let balances = - bee_wallet::multisig_balances(shellnet_endpoints(), result.address.clone()) - .await - .expect("multisig_balances failed"); + let balances = bee_wallet::multisig_balances(shellnet_endpoints(), result.address.clone()) + .await + .expect("multisig_balances failed"); println!("multisig_balances = {balances:?}"); - assert!( - balances.contains_key(&2), - "SHELL (ECC[2]) slot should be present, got {balances:?}" - ); + assert!(balances.contains_key(&2), "SHELL (ECC[2]) slot should be present, got {balances:?}"); } /// Brick 1 in isolation: address derivation is deterministic for fixed inputs @@ -4409,12 +4404,10 @@ async fn test_compute_multisig_address_deterministic() { constructor_value: "0".to_string(), }; - let a = bee_wallet::compute_multisig_address(ctx.clone(), &spec) - .await - .expect("compute address a"); - let b = bee_wallet::compute_multisig_address(ctx.clone(), &spec) - .await - .expect("compute address b"); + let a = + bee_wallet::compute_multisig_address(ctx.clone(), &spec).await.expect("compute address a"); + let b = + bee_wallet::compute_multisig_address(ctx.clone(), &spec).await.expect("compute address b"); assert_eq!(a, b, "address must be deterministic for fixed spec"); assert!(a.starts_with("0:")); @@ -4428,8 +4421,7 @@ async fn test_compute_multisig_address_deterministic() { req_confirms_data: 1, constructor_value: "0".to_string(), }; - let c = bee_wallet::compute_multisig_address(ctx, &other_spec) - .await - .expect("compute address c"); + let c = + bee_wallet::compute_multisig_address(ctx, &other_spec).await.expect("compute address c"); assert_ne!(a, c, "different keys must yield a different address"); } From 1f721e7bbf2054ea86808612b5b9821606f1d1d5 Mon Sep 17 00:00:00 2001 From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:52:05 +0200 Subject: [PATCH 3/5] Fix npm --- bee_sdk/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bee_sdk/Cargo.toml b/bee_sdk/Cargo.toml index cec73a6..ea399cb 100644 --- a/bee_sdk/Cargo.toml +++ b/bee_sdk/Cargo.toml @@ -3,6 +3,9 @@ name = "bee-sdk" version.workspace = true license.workspace = true edition.workspace = true +# wasm-pack copies this into pkg/package.json's `repository`; npm provenance +# (OIDC trusted publishing) rejects the publish unless it matches the source repo. +repository = "https://github.com/gosh-sh/bee-engine" [lib] crate-type = ["cdylib", "rlib"] From ef91bac15e749b5aed6a3d675267b4f15a43f050 Mon Sep 17 00:00:00 2001 From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:02:09 +0200 Subject: [PATCH 4/5] Add cache on failure --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 37cff1e..eebd465 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,6 +61,10 @@ jobs: - name: Cache Rust uses: Swatinem/rust-cache@v2 + with: + # Keep the compile cache even when a later step (e.g. npm publish) fails, + # so re-runs while debugging don't recompile from scratch. + cache-on-failure: true - name: Install wasm-pack run: cargo install wasm-pack --locked From c06fbcc2d2db91938912aab766e0eb68ee641b30 Mon Sep 17 00:00:00 2001 From: Andrei Shuvalov <6286552+dronbas@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:36:57 +0200 Subject: [PATCH 5/5] Update npm readme --- bee_sdk/README.md | 139 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 bee_sdk/README.md diff --git a/bee_sdk/README.md b/bee_sdk/README.md new file mode 100644 index 0000000..7fb3b44 --- /dev/null +++ b/bee_sdk/README.md @@ -0,0 +1,139 @@ +# @teamgosh/bee-sdk + +[![npm](https://img.shields.io/npm/v/@teamgosh/bee-sdk?color=cb3837&logo=npm)](https://www.npmjs.com/package/@teamgosh/bee-sdk) + +WebAssembly SDK for [Acki Nacki](https://ackinacki.com) — drive multifactor +wallets, mining, wallet-connect sessions, and flat-multisig deploy straight from +the browser. Compiled from the `bee-engine` Rust workspace with `wasm-pack` +(`web` target), fully typed. + +## Features + +- **Multifactor wallets** — deploy, query, manage factors, zk-login. +- **Mining** — resolve miner addresses, set mining keys, drive a miner. +- **Wallet-connect** — shared-key sessions, challenge/response, profile resolve. +- **Flat multisig** — fully client-side giver-funded deploy + ECC balance reads. +- **Typed end-to-end** — complete `.d.ts` ships with the package. + +> **Runtime:** browser / WebAssembly (built `wasm-pack --target web`). Not a +> Node package — it relies on the browser `WebAssembly` + `fetch` APIs. + +## Install + +```bash +npm i @teamgosh/bee-sdk +``` + +## Initialize + +WebAssembly must be initialized **once** before any other call. The package +ships the `bee_sdk_bg.wasm` binary; point `init` at it so your bundler emits and +serves it. Pick the snippet for your setup: + +**Vite** + +```ts +import init from "@teamgosh/bee-sdk"; +import wasmUrl from "@teamgosh/bee-sdk/bee_sdk_bg.wasm?url"; + +await init({ module_or_path: wasmUrl }); +``` + +**webpack 5 / Next.js** + +```ts +import init from "@teamgosh/bee-sdk"; + +const wasmUrl = new URL("@teamgosh/bee-sdk/bee_sdk_bg.wasm", import.meta.url); +await init({ module_or_path: wasmUrl }); +``` + +**Any setup (host the file yourself)** + +Copy `node_modules/@teamgosh/bee-sdk/bee_sdk_bg.wasm` into your static/public +assets, then pass its served URL: + +```ts +import init from "@teamgosh/bee-sdk"; + +await init({ module_or_path: "/assets/bee_sdk_bg.wasm" }); +``` + +Call `init` once at startup; everything below assumes it has resolved. + +## Usage + +### Flat multisig deploy (shellnet, fully client-side) + +Funds a fresh multisig address from the default shellnet giver, then deploys it. +Always returns the owner keypair — **persist `secret`**. All amounts are strings +(u64 / ECC values exceed `2^53` and would lose precision as JS numbers). + +```ts +import init, { deploy_multisig_via_giver, multisig_balances } from "@teamgosh/bee-sdk"; + +await init(); + +const res = await deploy_multisig_via_giver({ + endpoints: ["https://shellnet.ackinacki.org"], + // keys? — owner keypair; generated when omitted, always returned + // owners_pubkey? — custodians ["0x…"] (uint256[]), default [owner] + // req_confirms?, req_confirms_data? — default 1 + // giver_value? — SHELL (ECC[2]) gas top-up, default "10000000000" + // giver_ecc? — extra ECC, { currency_id: "amount" } + // wait_for_active? — wait until Active, default true +}); + +console.log(res.address); // 0x… canonical :: +saveSecretSomewhere(res.secret); + +// ECC balances of any account by address → { currency_id: raw_amount_string } +const balances = await multisig_balances({ + endpoints: ["https://shellnet.ackinacki.org"], + address: res.address, +}); +// e.g. { "2": "10000000000" } (1 = NACKL, 2 = SHELL, 3 = USDC) +``` + +### Multifactor wallet + +```ts +import init, { Wallet } from "@teamgosh/bee-sdk"; + +await init(); + +const wallet = new Wallet( + ["https://shellnet.ackinacki.org"], // endpoints + null, // archive endpoints (optional) + "https://app-backend.ackinacki.org/api", // bee-infra backend + "0x0000000000000000000000000000000000000000000000000000000000000000", // app id +); +``` + +### Wallet-connect & mining + +```ts +import init, { BeeConnect, get_miner_address_by_wallet_name } from "@teamgosh/bee-sdk"; + +await init(); + +const connect = new BeeConnect(); +const session = connect.create_shared_key_session(appId, 300, null); +// → present session.deep_link to the wallet app, then connect.wait_wallet_hello(...) + +const minerAddress = await get_miner_address_by_wallet_name({ + client_config: { network: { endpoints: ["https://shellnet.ackinacki.org"] } }, + wallet_name: "my-wallet", +}); +``` + +## Notes + +- Account addresses come back in canonical dApp-scoped form `::`. +- The giver-funded multisig deploy is **shellnet-only** (the default giver lives + only on shellnet) — gate it by network on your side. +- See `bee_sdk.d.ts` for the full, authoritative type surface. + +## License + +`LicenseRef-Acki-Nacki-Node-License` (see `license` in `package.json`).