├── lib ├── sube │ ├── sube-js │ │ ├── README.md │ │ ├── .npmignore │ │ ├── demo │ │ │ ├── src │ │ │ │ ├── vite-env.d.ts │ │ │ │ ├── sube.ts │ │ │ │ ├── main.ts │ │ │ │ └── style.css │ │ │ ├── .gitignore │ │ │ ├── vite.config.js │ │ │ ├── index.html │ │ │ ├── tsconfig.json │ │ │ ├── package.json │ │ │ └── public │ │ │ │ └── vite.svg │ │ ├── .gitignore │ │ ├── .appveyor.yml │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── util.rs │ │ │ └── lib.rs │ │ ├── tsconfig.json │ │ ├── Cargo.toml │ │ ├── LICENSE_MIT │ │ ├── package.json │ │ ├── package-lock.json │ │ └── .travis.yml │ ├── cli │ │ ├── .gitignore │ │ ├── README.md │ │ ├── Cargo.toml │ │ ├── Makefile │ │ └── src │ │ │ ├── opts.rs │ │ │ └── main.rs │ ├── justfile │ ├── examples │ │ ├── query_balance.rs │ │ ├── query_balance_builder.rs │ │ ├── query_at_block.rs │ │ ├── query_referendum_info.rs │ │ ├── query_preimage.rs │ │ ├── query_membership.rs │ │ ├── query_balance_ws.rs │ │ ├── query_identity.rs │ │ ├── send_tx_libwallet.rs │ │ ├── pallet_communities.rs │ │ └── send_tx_builder.rs │ ├── src │ │ ├── util.rs │ │ ├── signer.rs │ │ ├── http.rs │ │ └── hasher.rs │ ├── README.md │ └── Cargo.toml ├── libwallet │ ├── js │ │ ├── test │ │ │ ├── index.js │ │ │ └── wallet.test.js │ │ ├── .npmignore │ │ ├── .gitignore │ │ ├── src │ │ │ ├── lib.rs │ │ │ ├── util.rs │ │ │ └── wallet.rs │ │ ├── package.json │ │ ├── README.md │ │ └── Cargo.toml │ ├── src │ │ ├── account.rs │ │ ├── substrate_ext.rs │ │ ├── vault │ │ │ ├── README.md │ │ │ ├── simple.rs │ │ │ └── os.rs │ │ └── util.rs │ ├── justfile │ ├── examples │ │ ├── persisted_in_keyring.rs │ │ ├── persisted_in_pass.rs │ │ └── account_generation.rs │ ├── Cargo.toml │ └── README.md ├── pjs-rs │ ├── dist │ │ ├── pjs.wasm │ │ └── pjs_bg.wasm │ ├── Cargo.toml │ └── index.html └── scales │ ├── src │ └── registry.bin │ ├── justfile │ ├── Cargo.toml │ └── README.md ├── sdk └── js │ ├── .gitignore │ ├── .papi │ ├── descriptors │ │ ├── .gitignore │ │ └── package.json │ ├── metadata │ │ ├── kreivo.scale │ │ └── kusama.scale │ └── polkadot-api.json │ ├── src │ ├── utils │ │ ├── base64.browser.ts │ │ ├── error.ts │ │ └── base64.ts │ ├── storage │ │ ├── index.ts │ │ ├── InMemoryImpl.ts │ │ ├── IStorage.ts │ │ ├── LocalStorageImpl.ts │ │ └── SignerSerializer.ts │ ├── services │ │ └── userService.ts │ ├── index.web.ts │ ├── index.ts │ ├── index.node.ts │ ├── serverSdk.ts │ ├── types.ts │ ├── custom.ts │ └── membership.ts │ ├── jest-puppeteer.config.js │ ├── justfile │ ├── examples │ ├── client │ │ ├── vite.config.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ ├── README.md │ │ └── index.html │ └── server │ │ ├── docker-compose.yml │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── README.md │ │ └── src │ │ └── storage │ │ └── MongoDBStorage.ts │ ├── demo │ ├── .gitignore │ ├── vite.config.js │ ├── package.json │ ├── tsconfig.json │ ├── README.md │ ├── src │ │ └── storage │ │ │ ├── SessionStorageImpl.ts │ │ │ └── IndexedDBStorage.ts │ └── index.html │ ├── jest.config.js │ ├── vite.config.mts │ ├── tsconfig.cjs.json │ ├── global.d.ts │ ├── tsconfig.node.json │ ├── tsconfig.esm.json │ ├── tsconfig.web.json │ ├── vite.config.esm.mts │ ├── vite.config.web.mts │ ├── vite.config.node.mts │ ├── tsconfig.json │ └── package.json ├── components ├── cube.png ├── virto-connect.gif ├── web-test-runner.config.js ├── utils.js ├── main.js ├── package.json ├── globalStyles.js ├── example.minimal.html ├── virto-notification │ ├── notification.js │ └── notification.test.js ├── avatar.js ├── button.js ├── components.html └── input.js ├── .gitignore ├── vos-programs └── memberships │ ├── .cargo │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ └── src │ └── main.rs ├── package.json ├── justfile ├── .github └── workflows │ ├── scales-pr.yml │ ├── sube-pr.yml │ ├── libwallet-pr.yml │ ├── general_checks.yml │ ├── javascript.yml │ ├── components.yml │ ├── sdk-pr.yml │ ├── libwallet-publish.yml │ ├── sube-publish.yml │ └── sdk-publish.yml └── README.md /lib/sube/sube-js/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/sube/cli/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /sdk/js/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .vscode/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /lib/libwallet/js/test/index.js: -------------------------------------------------------------------------------- 1 | import './wallet.test.js'; 2 | -------------------------------------------------------------------------------- /lib/sube/sube-js/.npmignore: -------------------------------------------------------------------------------- 1 | **/*.rs 2 | Cargo.toml 3 | pkg-*/package.json -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /lib/libwallet/js/.npmignore: -------------------------------------------------------------------------------- 1 | **/*.rs 2 | Cargo.toml 3 | pkg/package.json 4 | !dist/**/* -------------------------------------------------------------------------------- /sdk/js/.papi/descriptors/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !package.json 4 | !dist/ 5 | -------------------------------------------------------------------------------- /components/cube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/components/cube.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | .vscode/ 4 | **/*.rs.bk 5 | dist/ 6 | .vscode/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /lib/libwallet/js/.gitignore: -------------------------------------------------------------------------------- 1 | # Wasm-generated package 2 | pkg/ 3 | 4 | # Node.js modules 5 | node_modules/ -------------------------------------------------------------------------------- /lib/pjs-rs/dist/pjs.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/lib/pjs-rs/dist/pjs.wasm -------------------------------------------------------------------------------- /lib/pjs-rs/dist/pjs_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/lib/pjs-rs/dist/pjs_bg.wasm -------------------------------------------------------------------------------- /lib/scales/src/registry.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/lib/scales/src/registry.bin -------------------------------------------------------------------------------- /components/virto-connect.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/components/virto-connect.gif -------------------------------------------------------------------------------- /sdk/js/.papi/metadata/kreivo.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/sdk/js/.papi/metadata/kreivo.scale -------------------------------------------------------------------------------- /sdk/js/.papi/metadata/kusama.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virto-network/virto-sdk/HEAD/sdk/js/.papi/metadata/kusama.scale -------------------------------------------------------------------------------- /lib/libwallet/js/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | pub(crate) mod util; 4 | pub(crate) mod wallet; 5 | -------------------------------------------------------------------------------- /lib/sube/sube-js/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | pkg-* 8 | node_modules 9 | dist -------------------------------------------------------------------------------- /vos-programs/memberships/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasip2" 3 | 4 | [target.wasm32-wasip2] 5 | runner = "wasmtime -Sinherit-network" 6 | -------------------------------------------------------------------------------- /lib/sube/justfile: -------------------------------------------------------------------------------- 1 | default: check 2 | 3 | check: 4 | cargo check --features http,wss 5 | 6 | lint: 7 | cargo clippy --features http,wss -- -D warnings 8 | -------------------------------------------------------------------------------- /lib/libwallet/src/account.rs: -------------------------------------------------------------------------------- 1 | use crate::{Public, Signer}; 2 | 3 | pub trait Account: Signer + core::fmt::Display { 4 | fn public(&self) -> impl Public; 5 | } 6 | -------------------------------------------------------------------------------- /lib/scales/justfile: -------------------------------------------------------------------------------- 1 | default: check 2 | 3 | check: 4 | cargo check 5 | 6 | lint: 7 | cargo clippy -- -D warnings 8 | 9 | test: 10 | cargo test 11 | -------------------------------------------------------------------------------- /lib/libwallet/js/src/util.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | #[wasm_bindgen] 4 | extern "C" { 5 | #[wasm_bindgen(js_namespace = console)] 6 | pub fn log(s: &str); 7 | } 8 | -------------------------------------------------------------------------------- /lib/sube/cli/README.md: -------------------------------------------------------------------------------- 1 | # Sube-cli 2 | 3 | For convenience Sube is also a stand-alone cli. 4 | 5 | [![asciicast](https://asciinema.org/a/443014.svg)](https://asciinema.org/a/443014) 6 | 7 | -------------------------------------------------------------------------------- /components/web-test-runner.config.js: -------------------------------------------------------------------------------- 1 | import { summaryReporter } from '@web/test-runner'; 2 | 3 | export default { 4 | files: ['virto-**/*.test.js'], 5 | nodeResolve: true, 6 | reporters: [ 7 | summaryReporter() 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /sdk/js/src/utils/base64.browser.ts: -------------------------------------------------------------------------------- 1 | import 'es-arraybuffer-base64/Uint8Array.fromBase64/auto'; 2 | 3 | export function fromBase64Url(str: string): ArrayBuffer { 4 | return Uint8Array.fromBase64(str, { alphabet: 'base64url' }) as unknown as ArrayBuffer; 5 | } 6 | -------------------------------------------------------------------------------- /sdk/js/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | export class VError extends Error { 2 | constructor(public code: string, message: string) { 3 | super(message); 4 | this.name = "VError"; 5 | 6 | Object.setPrototypeOf(this, VError.prototype); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sdk/js/src/storage/index.ts: -------------------------------------------------------------------------------- 1 | export type { IStorage } from "./IStorage"; 2 | export { LocalStorageImpl } from "./LocalStorageImpl"; 3 | export { InMemoryImpl } from "./InMemoryImpl"; 4 | export { SignerSerializer, type SerializableSignerData } from "./SignerSerializer"; -------------------------------------------------------------------------------- /vos-programs/memberships/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "memberships" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | vos = { git = "https://codeberg.org/Virto/vos.git", features = ["bin"] } 8 | 9 | [features] 10 | stand-alone = [] 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@polkadot-api/substrate-bindings": "^0.14.0" 4 | }, 5 | "packageManager": "pnpm@10.18.3+sha512.bbd16e6d7286fd7e01f6b3c0b3c932cda2965c06a908328f74663f10a9aea51f1129eea615134bf992831b009eabe167ecb7008b597f40ff9bc75946aadfb08d" 6 | } 7 | -------------------------------------------------------------------------------- /sdk/js/jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | launch: { 3 | headless: true, 4 | args: ['--no-sandbox', '--disable-setuid-sandbox'], 5 | }, 6 | server: { 7 | command: 'npm run dev', 8 | port: 3000, 9 | launchTimeout: 10000, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /sdk/js/justfile: -------------------------------------------------------------------------------- 1 | default: check 2 | 3 | check: 4 | npm ci 5 | npm run build 6 | 7 | lint: 8 | npm run lint 9 | 10 | test: 11 | npm ci 12 | npm run test 13 | 14 | publish: 15 | npm ci 16 | npm run build 17 | npm version patch 18 | npm publish --access public 19 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | default: check 2 | 3 | check: check-sube check-scales check-libwallet 4 | 5 | check-sube: 6 | @just -f lib/sube/justfile check lint 7 | 8 | check-scales: 9 | @just -f lib/scales/justfile check lint 10 | 11 | check-libwallet: 12 | @just -f lib/libwallet/justfile check lint 13 | -------------------------------------------------------------------------------- /lib/libwallet/justfile: -------------------------------------------------------------------------------- 1 | default: 2 | just --choose 3 | 4 | check: 5 | cargo check 6 | lint: 7 | cargo clippy -- -D warnings 8 | 9 | check-no-std: 10 | cargo build --features substrate --target wasm32-unknown-unknown 11 | cargo build --features substrate --target riscv32i-unknown-none-elf 12 | -------------------------------------------------------------------------------- /sdk/js/examples/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import wasm from 'vite-plugin-wasm' 3 | 4 | export default defineConfig({ 5 | plugins: [wasm()], 6 | server: { 7 | port: 5173, 8 | host: true 9 | }, 10 | optimizeDeps: { 11 | exclude: ['@virtonetwork/libwallet'] 12 | } 13 | }) -------------------------------------------------------------------------------- /vos-programs/memberships/README.md: -------------------------------------------------------------------------------- 1 | # Memberships API 2 | 3 | A VOS program defining means for an organization to manage their users' memberships 4 | 5 | > **WARNING!** At the time of writting, the code in this crate is pseudo-code for showcase only. 6 | > Once VOS mvp is realeased the code will be updated to have a usable program that can be published 7 | -------------------------------------------------------------------------------- /components/utils.js: -------------------------------------------------------------------------------- 1 | export const tagFn = fn => (strings, ...parts) => fn(parts.reduce((tpl, value, i) => `${tpl}${strings[i]}${value}`, '').concat(strings[parts.length])) 2 | export const html = tagFn(s => new DOMParser().parseFromString(``, 'text/html').querySelector('template')) 3 | export const css = tagFn(s => new CSSStyleSheet().replace(s)) -------------------------------------------------------------------------------- /components/main.js: -------------------------------------------------------------------------------- 1 | import 'https://early.webawesome.com/webawesome@3.0.0-alpha.11/dist/components/avatar/avatar.js' 2 | import "https://early.webawesome.com/webawesome@3.0.0-alpha.11/dist/components/button/button.js" 3 | import "https://early.webawesome.com/webawesome@3.0.0-alpha.11/dist/components/input/input.js" 4 | 5 | import "./avatar.js" 6 | import "./button.js" 7 | import "./input.js" -------------------------------------------------------------------------------- /lib/sube/examples/query_balance.rs: -------------------------------------------------------------------------------- 1 | use core::future::{Future, IntoFuture}; 2 | use sube::{sube, Response, Result}; 3 | 4 | #[async_std::main] 5 | async fn main() -> Result<()> { 6 | let result = sube!("wss://rococo-rpc.polkadot.io/system/account/0x3c85f79f28628bee75cdb9eddfeae249f813fad95f84120d068fbc990c4b717d").await?; 7 | 8 | println!("{:?}", result); 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /sdk/js/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .vscode -------------------------------------------------------------------------------- /lib/sube/sube-js/.appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 | - rustc -V 6 | - cargo -V 7 | 8 | build: false 9 | 10 | test_script: 11 | - cargo test --locked 12 | -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | 11 | node_modules 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | .vscode -------------------------------------------------------------------------------- /sdk/js/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-puppeteer', 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | testEnvironment: 'jest-environment-puppeteer', 7 | testMatch: ['**/*.test.ts', '**/*.test.js', '**/*.spec.ts', '**/*.spec.js', '**/*.e2e.ts', '**/*.e2e.js'], 8 | moduleFileExtensions: ['ts', 'js'], 9 | roots: ['/src', '/test'], 10 | testTimeout: 300000, 11 | }; 12 | -------------------------------------------------------------------------------- /lib/pjs-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pjs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | wasm-bindgen = { version = "0.2.92", default-features = false } 8 | wasm-bindgen-futures = "0.4.42" 9 | 10 | [dependencies.web-sys] 11 | version = "0.3.69" 12 | features = [ 13 | "Window", 14 | ] 15 | 16 | [features] 17 | default = ["js"] 18 | js = [] 19 | 20 | [lib] 21 | crate-type = ["cdylib"] 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import wasm from "vite-plugin-wasm"; 2 | import topLevelAwait from "vite-plugin-top-level-await"; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | wasm(), 8 | topLevelAwait() 9 | ], 10 | server: { 11 | fs: { 12 | allow: ['../dist', '.', '/Users/davidbarinas/db/virto/libwallet/libwallet-js/pkg'] 13 | } 14 | } 15 | }); -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sube-js 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "components", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "web-test-runner \"virto-**/*.test.js\" --node-resolve" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@esm-bundle/chai": "^4.3.4-fix.0", 15 | "@web/test-runner": "^0.20.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sdk/js/examples/server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:7.0 6 | container_name: virto-mongodb 7 | restart: unless-stopped 8 | ports: 9 | - "27017:27017" 10 | volumes: 11 | - mongodb_data:/data/db 12 | networks: 13 | - virto-network 14 | command: mongod --noauth 15 | 16 | volumes: 17 | mongodb_data: 18 | 19 | networks: 20 | virto-network: 21 | driver: bridge -------------------------------------------------------------------------------- /lib/libwallet/src/substrate_ext.rs: -------------------------------------------------------------------------------- 1 | use crate::Network; 2 | 3 | impl From<&str> for Network { 4 | fn from(s: &str) -> Self { 5 | // TODO use registry 6 | match s { 7 | "polkadot" => Network::Substrate(0), 8 | "kusama" => Network::Substrate(2), 9 | "karura" => Network::Substrate(8), 10 | "substrate" => Network::Substrate(42), 11 | _ => Network::default(), 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/sube/examples/query_balance_builder.rs: -------------------------------------------------------------------------------- 1 | use env_logger; 2 | use sube::{sube, ExtrinsicBody, Response, Result, SubeBuilder}; 3 | 4 | #[async_std::main] 5 | async fn main() -> Result<()> { 6 | env_logger::init(); 7 | 8 | let builder = SubeBuilder::default() 9 | .with_url("wss://rococo-rpc.polkadot.io/system/account/0x3c85f79f28628bee75cdb9eddfeae249f813fad95f84120d068fbc990c4b717d") 10 | .await?; 11 | 12 | println!("{:?}", builder); 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /sdk/js/vite.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import wasm from 'vite-plugin-wasm'; 4 | 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [wasm()], 10 | ssr: { 11 | external: ['jsonwebtoken'] 12 | }, 13 | build: { 14 | outDir: "dist/umd", 15 | lib: { 16 | entry: resolve(__dirname, "src/index.ts"), 17 | name: "SDK", 18 | fileName: "index", 19 | formats: ["umd"] 20 | } 21 | } 22 | }); -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "noEmit": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "skipLibCheck": true 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /sdk/js/examples/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virto-client-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@virtonetwork/libwallet": "^1.0.5", 13 | "@virtonetwork/sdk": "file:../..", 14 | "@virtonetwork/sube": "^1.0.0-alpha.2" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5.3.3", 18 | "vite": "^5.0.8", 19 | "vite-plugin-wasm": "^3.4.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/sube/sube-js/src/index.ts: -------------------------------------------------------------------------------- 1 | import { sube_js } from 'sube-js'; 2 | 3 | export interface SubeOptions { 4 | sign: (message: Uint8Array) => Promise, 5 | from: Uint8Array, 6 | body: any, 7 | nonce?: number, 8 | } 9 | 10 | 11 | export async function sube(url: string, options?: SubeOptions) { 12 | return sube_js(url, options && { 13 | from: options.from, 14 | call: { 15 | nonce: options.nonce, 16 | body: options.body, 17 | } 18 | }, options ? function (i: Uint8Array) { 19 | return options.sign(i); 20 | } : () => {}) as Promise; 21 | } 22 | -------------------------------------------------------------------------------- /sdk/js/examples/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2020", "DOM"], 4 | "target": "ES2020", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./dist", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@virtonetwork/sdk/descriptors": ["../../.papi/descriptors/dist/index.d.ts"] 11 | }, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "skipLibCheck": true, 15 | "forceConsistentCasingInFileNames": true 16 | }, 17 | "include": ["src/**/*", "../../src/**/*"], 18 | "exclude": ["node_modules"] 19 | } -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "typescript": "^4.9.3", 13 | "vite": "^4.0.0" 14 | }, 15 | "dependencies": { 16 | "@virtonetwork/libwallet": "file:../../../libwallet/js", 17 | "@virtonetwork/sube": "file:../", 18 | "ts-node": "^10.9.1", 19 | "vite-plugin-top-level-await": "^1.2.2", 20 | "vite-plugin-wasm": "^3.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/sube/src/util.rs: -------------------------------------------------------------------------------- 1 | use alloc::string::String; 2 | 3 | pub fn to_camel(term: &str) -> String { 4 | let underscore_count = term.chars().filter(|c| *c == '-').count(); 5 | let mut result = String::with_capacity(term.len() - underscore_count); 6 | let mut at_new_word = true; 7 | 8 | for c in term.chars().skip_while(|&c| c == '-') { 9 | if c == '-' { 10 | at_new_word = true; 11 | } else if at_new_word { 12 | result.push(c.to_ascii_uppercase()); 13 | at_new_word = false; 14 | } else { 15 | result.push(c); 16 | } 17 | } 18 | result 19 | } 20 | -------------------------------------------------------------------------------- /sdk/js/demo/vite.config.js: -------------------------------------------------------------------------------- 1 | import wasm from "vite-plugin-wasm"; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | wasm() 7 | ], 8 | build: { 9 | target: "ES2022", // Soporta top-level await nativamente 10 | rollupOptions: { 11 | output: { 12 | format: 'es' // Usar formato ES modules 13 | } 14 | } 15 | }, 16 | optimizeDeps: { 17 | esbuildOptions: { 18 | target: "esnext" // Configurar esbuild para soportar top-level await 19 | } 20 | }, 21 | server: { 22 | port: 3000, 23 | fs: { 24 | allow: ['../dist', '.'] 25 | } 26 | } 27 | }); -------------------------------------------------------------------------------- /.github/workflows/scales-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: ['scales/**'] 4 | 5 | name: Scales 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: extractions/setup-just@v2 13 | - name: Check 14 | run: just -f lib/scales/justfile check 15 | - name: Lint 16 | run: just -f lib/scales/justfile lint 17 | 18 | test: 19 | runs-on: ubuntu-latest 20 | needs: check 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: extractions/setup-just@v2 24 | - name: Test 25 | run: just -f lib/scales/justfile test 26 | -------------------------------------------------------------------------------- /sdk/js/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "ES2020", 5 | "outDir": "./dist/cjs", 6 | "rootDir": "./src", 7 | "declaration": true, 8 | "declarationDir": "./dist/cjs", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "baseUrl": "./", 13 | "moduleResolution": "node", 14 | "paths": { 15 | "@/*": [ 16 | "src/*" 17 | ], 18 | "@virtonetwork/sdk/descriptors": [ 19 | ".papi/descriptors/dist/index.d.ts" 20 | ] 21 | } 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | "global.d.ts" 26 | ] 27 | } -------------------------------------------------------------------------------- /sdk/js/src/storage/InMemoryImpl.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "./IStorage"; 2 | 3 | export class InMemoryImpl implements IStorage { 4 | private storage: Map = new Map(); 5 | 6 | store(key: string, session: T): void { 7 | this.storage.set(key, session); 8 | } 9 | 10 | get(key: string): T | null { 11 | return this.storage.get(key) || null; 12 | } 13 | 14 | getAll(): T[] { 15 | return Array.from(this.storage.values()); 16 | } 17 | 18 | remove(key: string): boolean { 19 | return this.storage.delete(key); 20 | } 21 | 22 | clear(): void { 23 | this.storage.clear(); 24 | } 25 | } -------------------------------------------------------------------------------- /.github/workflows/sube-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: ['lib/sube/**'] 4 | 5 | name: Sube 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: extractions/setup-just@v2 13 | 14 | - name: Install Rust 15 | run: | 16 | curl https://sh.rustup.rs -sSf | sh -s -- -y 17 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 18 | source "$HOME/.cargo/env" 19 | 20 | - name: Check 21 | working-directory: lib/sube 22 | run: just check 23 | 24 | - name: Lint 25 | working-directory: lib/sube 26 | run: just lint 27 | -------------------------------------------------------------------------------- /sdk/js/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview" 9 | }, 10 | "devDependencies": { 11 | "typescript": "^4.9.3", 12 | "vite": "^4.0.0" 13 | }, 14 | "dependencies": { 15 | "@polkadot/util-crypto": "^13.4.3", 16 | "@virtonetwork/libwallet": "file:../../../lib/libwallet/js", 17 | "@virtonetwork/sube": "file:../../../lib/sube/sube-js", 18 | "idb": "^8.0.3", 19 | "ts-node": "^10.9.1", 20 | "vite-plugin-top-level-await": "^1.2.2", 21 | "vite-plugin-wasm": "^3.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sdk/js/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ESNext", "DOM"], 7 | "moduleResolution": "node", 8 | "baseUrl": "../", 9 | "paths": { 10 | "@virtonetwork/sdk/descriptors": ["../.papi/descriptors/dist/index.d.ts"] 11 | }, 12 | "strict": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "esModuleInterop": true, 16 | "noEmit": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "skipLibCheck": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /lib/sube/cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anyhow = "1.0.57" 8 | async-std = "1.11.0" 9 | serde_json = "1.0.80" 10 | stderrlog = "0.5.1" 11 | structopt = "0.3.26" 12 | url = "2.2.2" 13 | log = "0.4.17" 14 | serde = { version = "1.0.137", default-features = false } 15 | codec = { version = "3.1.2", package = "parity-scale-codec", default-features = false } 16 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] } 17 | 18 | [dependencies.sube] 19 | path = ".." 20 | features = [ 21 | "std", 22 | "http", 23 | # "wss", 24 | ] 25 | 26 | [[bin]] 27 | name = "sube" 28 | path = "src/main.rs" 29 | -------------------------------------------------------------------------------- /sdk/js/.papi/descriptors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0-autogenerated.9472628191967840292", 3 | "name": "@polkadot-api/descriptors", 4 | "files": [ 5 | "dist" 6 | ], 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "module": "./dist/index.mjs", 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs", 18 | "browser": "./dist/index.mjs", 19 | "types": "./dist/index.d.ts", 20 | "sideEffects": false, 21 | "peerDependencies": { 22 | "polkadot-api": ">=1.11.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/libwallet-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: ['lib/libwallet/**'] 4 | 5 | name: Libwallet 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: extractions/setup-just@v2 13 | 14 | - name: Install Rust 15 | run: | 16 | curl https://sh.rustup.rs -sSf | sh -s -- -y 17 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 18 | source "$HOME/.cargo/env" 19 | 20 | - name: Check 21 | working-directory: lib/libwallet 22 | run: just check 23 | 24 | - name: Lint 25 | working-directory: lib/libwallet 26 | run: just lint 27 | 28 | -------------------------------------------------------------------------------- /sdk/js/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'jest-environment-puppeteer'; 2 | import { SubeOptions } from '@virtonetwork/sube'; 3 | declare global { 4 | const page: import('puppeteer').Page; 5 | const browser: import('puppeteer').Browser; 6 | const jestPuppeteer: import('jest-puppeteer').Global; 7 | } 8 | 9 | import { SDK } from "./src/sdk"; 10 | 11 | declare global { 12 | interface Window { 13 | SDK: typeof SDK; 14 | WalletType: typeof WalletType; 15 | mockSube(url: string, options?: SubeOptions): Promise; 16 | jsWalletFn(mnemonic?: string): any; 17 | } 18 | interface Uint8ArrayConstructor { 19 | fromBase64(base64: string, options: Record): Uint8Array; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/sube/cli/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test test-internal 2 | 3 | DOCKER=podman 4 | CONTAINER_IMAGE=parity/polkadot:v0.9.19 5 | CONTAINER_NAME=test-blockchain-node 6 | TEST_REQUEST='{"method": "system_version"}' 7 | 8 | default: test 9 | 10 | test: 11 | @bash -c "trap '$(DOCKER) kill $(CONTAINER_NAME)' EXIT; $(MAKE) -s test-internal" 12 | 13 | test-internal: 14 | @echo "⚒️ Running test node container" 15 | $(DOCKER) run -d -p 12345:9933 -p 24680:9944 --rm --name $(CONTAINER_NAME) \ 16 | $(CONTAINER_IMAGE) --dev --rpc-external --ws-external 17 | @echo "⏳ Waiting node to be ready" 18 | @curl localhost:12345 -fs --retry 5 --retry-all-errors -H 'Content-Type: application/json' -d $(TEST_REQUEST) 19 | cargo test 20 | -------------------------------------------------------------------------------- /lib/sube/examples/query_at_block.rs: -------------------------------------------------------------------------------- 1 | use env_logger; 2 | use serde_json; 3 | use sube::{sube, ExtrinsicBody, Response, Result, SubeBuilder}; 4 | 5 | #[async_std::main] 6 | async fn main() -> Result<()> { 7 | env_logger::init(); 8 | 9 | let result = sube!("ws://127.0.0.1:12281/system/account/0x12840f0626ac847d41089c4e05cf0719c5698af1e3bb87b66542de70b2de4b2b?at=2067321").await?; 10 | 11 | if let Response::Value(value) = result { 12 | let data = serde_json::to_value(&value).expect("to be serializable"); 13 | println!( 14 | "Account info: {}", 15 | serde_json::to_string_pretty(&data).expect("it must return an str") 16 | ); 17 | } 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /lib/libwallet/examples/persisted_in_keyring.rs: -------------------------------------------------------------------------------- 1 | use libwallet::{self, vault, Language}; 2 | 3 | use std::{env, error::Error}; 4 | 5 | type Wallet = libwallet::Wallet>; 6 | 7 | const TEST_USER: &str = "test_user"; 8 | 9 | #[async_std::main] 10 | async fn main() -> Result<(), Box> { 11 | let pin = env::args().nth(1); 12 | let pin = pin.as_ref().map(String::as_str); 13 | 14 | let vault = vault::OSKeyring::::new(TEST_USER, Language::default()); 15 | 16 | let mut wallet = Wallet::new(vault); 17 | 18 | wallet.unlock(None, pin).await?; 19 | 20 | let account = wallet.default_account(); 21 | println!("Default account: {}", account.unwrap()); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /sdk/js/examples/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "baseUrl": "../../", 14 | "paths": { 15 | "@virtonetwork/sdk/descriptors": ["../../.papi/descriptors/dist/index.d.ts"] 16 | }, 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } -------------------------------------------------------------------------------- /sdk/js/.papi/polkadot-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0, 3 | "descriptorPath": ".papi/descriptors", 4 | "entries": { 5 | "kreivo": { 6 | "wsUrl": "wss://testnet.kreivo.kippu.rocks", 7 | "metadata": ".papi/metadata/kreivo.scale", 8 | "genesis": "0x5cdd465e1273859192b043721e63ad19c8585f7c9779c53cba1d3260549b60ad", 9 | "codeHash": "0x55214ee38491baac38fc5e5b6ac366a14124fd042f03b030414bf0ce70b8dcc9" 10 | }, 11 | "kusama": { 12 | "chain": "ksmcc3", 13 | "metadata": ".papi/metadata/kusama.scale", 14 | "genesis": "0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe", 15 | "codeHash": "0xf45689276dfdcf05b6defd26da68e3283a96190d789f7a311ff31f64b455b421" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/general_checks.yml: -------------------------------------------------------------------------------- 1 | name: General checks 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | sube: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: extractions/setup-just@v2 15 | - name: Check sube 16 | run: just check-sube 17 | 18 | scales: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: extractions/setup-just@v2 23 | - name: Check 24 | run: just check-scales 25 | 26 | libwallet: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: extractions/setup-just@v2 31 | - name: Check 32 | run: just check-libwallet 33 | -------------------------------------------------------------------------------- /sdk/js/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "ES2020", 5 | "outDir": "./dist/node", 6 | "rootDir": "./src", 7 | "declaration": true, 8 | "declarationDir": "./dist/node", 9 | "declarationMap": true, 10 | "emitDeclarationOnly": true, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": [ 18 | "src/*" 19 | ], 20 | "@virtonetwork/sdk/descriptors": [ 21 | ".papi/descriptors/dist/index.d.ts" 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "src/index.node.ts", 27 | "src/**/*", 28 | "global.d.ts" 29 | ] 30 | } 31 | 32 | -------------------------------------------------------------------------------- /.github/workflows/javascript.yml: -------------------------------------------------------------------------------- 1 | name: JS 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | - name: Prepare dependencies 20 | run: | 21 | rustup target add wasm32-unknown-unknown 22 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 23 | - name: Build 24 | working-directory: js 25 | run: | 26 | wasm-pack build --dev --target nodejs --all-features 27 | - name: Run tests 28 | working-directory: js/test 29 | run: | 30 | npm ci 31 | npm test 32 | -------------------------------------------------------------------------------- /lib/libwallet/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtonetwork/libwallet", 3 | "version": "1.0.5", 4 | "description": "Exposes libwallet to be used on Javascript environments", 5 | "type": "module", 6 | "scripts": { 7 | "build": "wasm-pack build --target bundler --out-dir dist/pkg --features js,vault_simple && rm -f dist/pkg/.gitignore", 8 | "test": "node test/index.js" 9 | }, 10 | "main": "./dist/pkg/libwallet_js.js", 11 | "types": "./dist/pkg/libwallet_js.d.ts", 12 | "files": [ 13 | "dist/" 14 | ], 15 | "exports": { 16 | ".": { 17 | "types": "./dist/pkg/libwallet_js.d.ts", 18 | "import": "./dist/pkg/libwallet_js.js", 19 | "default": "./dist/pkg/libwallet_js.js" 20 | } 21 | }, 22 | "publishConfig": { 23 | "access": "public" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/sube/examples/query_referendum_info.rs: -------------------------------------------------------------------------------- 1 | use env_logger; 2 | use serde_json; 3 | use sube::{sube, ExtrinsicBody, Response, Result, SubeBuilder}; 4 | 5 | #[async_std::main] 6 | async fn main() -> Result<()> { 7 | env_logger::init(); 8 | 9 | let query = format!( 10 | "https://kreivo.io/communityReferenda/referendumInfoFor/{}", 11 | 24 12 | ); 13 | 14 | let r = sube!(&query).await?; 15 | 16 | if let Response::Value(ref v) = r { 17 | let json_value = serde_json::to_value(v).expect("it must to be an valid Value"); 18 | println!("Raw JSON value: {:?}", json_value); 19 | println!( 20 | "Info: {}", 21 | serde_json::to_string_pretty(&json_value).expect("it must return an str") 22 | ); 23 | } 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /lib/libwallet/src/vault/README.md: -------------------------------------------------------------------------------- 1 | # Vaults 2 | 3 | To support a wide variety of platforms `libwallet` has the concept of a vault, 4 | an abstraction used to retreive the private keys used for signing. 5 | 6 | ## Backends 7 | 8 | ### Simple 9 | 10 | An in memmory key storage that will forget keys at the end of a program's 11 | execution. It's useful for tests and generating addresses. 12 | 13 | ### OS Keyring 14 | 15 | A cross platform storage that uses the operating system's default keyring to 16 | store the secret seed used to generate accounts. Useful for desktop wallets. 17 | 18 | ### Pass 19 | 20 | A cross platform secret vault storage that uses pass-like implementation (using 21 | GPG as backend) to encrypt the secret seed used to generate accounts. Requires 22 | `gnupg` or `gpgme` as dependencies. 23 | -------------------------------------------------------------------------------- /sdk/js/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "ES2020", 5 | "outDir": "./dist/esm", 6 | "rootDir": "./src", 7 | "declaration": true, 8 | "declarationDir": "./dist/esm", 9 | "declarationMap": true, 10 | "emitDeclarationOnly": true, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": [ 18 | "src/*" 19 | ], 20 | "@virtonetwork/sdk/descriptors": [ 21 | ".papi/descriptors/dist/index.d.ts" 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "src/**/*", 27 | "global.d.ts" 28 | ], 29 | "exclude": [ 30 | "src/serverAuth.ts", 31 | "src/serverSdk.ts" 32 | ] 33 | } -------------------------------------------------------------------------------- /sdk/js/tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ES2020", 4 | "target": "ES2020", 5 | "outDir": "./dist/web", 6 | "rootDir": "./src", 7 | "declaration": true, 8 | "declarationDir": "./dist/web", 9 | "declarationMap": true, 10 | "emitDeclarationOnly": true, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "paths": { 17 | "@/*": [ 18 | "src/*" 19 | ], 20 | "@virtonetwork/sdk/descriptors": [ 21 | ".papi/descriptors/dist/index.d.ts" 22 | ] 23 | } 24 | }, 25 | "include": [ 26 | "src/index.web.ts", 27 | "src/**/*", 28 | "global.d.ts" 29 | ], 30 | "exclude": [ 31 | "src/serverAuth.ts", 32 | "src/serverSdk.ts" 33 | ] 34 | } 35 | 36 | -------------------------------------------------------------------------------- /lib/libwallet/examples/persisted_in_pass.rs: -------------------------------------------------------------------------------- 1 | use dirs::home_dir; 2 | use libwallet::{self, vault::Pass, Language}; 3 | use std::error::Error; 4 | type PassVault = Pass; 5 | type Wallet = libwallet::Wallet; 6 | 7 | #[async_std::main] 8 | async fn main() -> Result<(), Box> { 9 | // first argument is used as account 10 | let account = std::env::args().skip(1).next().unwrap_or("default".into()); 11 | let mut store_path = home_dir().expect("Could not find home path"); 12 | store_path.push(".password-store"); 13 | 14 | let vault = Pass::new(store_path.to_str().unwrap(), Language::default()); 15 | let mut wallet = Wallet::new(vault); 16 | 17 | wallet.unlock(None, account).await?; 18 | 19 | let account = wallet.default_account(); 20 | println!("Default account: {}", account.unwrap()); 21 | 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /lib/sube/examples/query_preimage.rs: -------------------------------------------------------------------------------- 1 | use core::future::{Future, IntoFuture}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{from_value, Value}; 4 | use sube::{sube, Response}; 5 | 6 | #[async_std::main] 7 | async fn main() -> sube::Result<()> { 8 | env_logger::init(); 9 | 10 | let query = format!( 11 | "ws://127.0.0.1:12281/preimage/preimageFor/{}/{}", 12 | "0x6b172c3695dca229e71c0bca790f5991b68f8eee96334e842312a0a7d4a46c6c", 30 13 | ); 14 | 15 | let r = sube!(&query).await?; 16 | 17 | if let Response::Value(ref v) = r { 18 | let json_value = serde_json::to_value(v).expect("to be serializable"); 19 | println!("json: {:?}", json_value); 20 | let x = serde_json::to_string_pretty(&json_value).expect("it must return an str"); 21 | println!("Account info: {:?}", x); 22 | } 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /sdk/js/vite.config.esm.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import wasm from 'vite-plugin-wasm'; 4 | 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [wasm()], 10 | ssr: { 11 | external: ['jsonwebtoken'] 12 | }, 13 | build: { 14 | outDir: "dist/esm", 15 | lib: { 16 | entry: resolve(__dirname, "src/index.web.ts"), 17 | name: "SDK", 18 | fileName: "index", 19 | formats: ["es"] 20 | }, 21 | rollupOptions: { 22 | external: [ 23 | 'jsonwebtoken', 24 | 'ws', 25 | 'crypto', 26 | 'stream', 27 | 'util', 28 | 'buffer', 29 | 'fs', 30 | 'path', 31 | 'os' 32 | ], 33 | output: { 34 | entryFileNames: "index.js", 35 | format: "es" 36 | } 37 | } 38 | } 39 | }); -------------------------------------------------------------------------------- /lib/sube/examples/query_membership.rs: -------------------------------------------------------------------------------- 1 | use core::future::{Future, IntoFuture}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{from_value, Value}; 4 | use sube::{sube, Response}; 5 | 6 | #[async_std::main] 7 | async fn main() -> sube::Result<()> { 8 | env_logger::init(); 9 | 10 | let query = format!( 11 | "ws://127.0.0.1:12281/communityMemberships/account/{}/{}", 12 | "0x12840f0626ac847d41089c4e05cf0719c5698af1e3bb87b66542de70b2de4b2b", 1 13 | ); 14 | 15 | let r = sube!(&query).await?; 16 | 17 | if let Response::ValueSet(ref v) = r { 18 | let json_value = serde_json::to_value(v).expect("to be serializable"); 19 | println!("json: {:?}", json_value); 20 | let x = serde_json::to_string_pretty(&json_value).expect("it must return an str"); 21 | println!("Account info: {:?}", x); 22 | } 23 | 24 | Ok(()) 25 | } 26 | -------------------------------------------------------------------------------- /sdk/js/vite.config.web.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import wasm from 'vite-plugin-wasm'; 4 | 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [wasm()], 10 | build: { 11 | outDir: "dist/web", 12 | lib: { 13 | entry: resolve(__dirname, "src/index.web.ts"), 14 | name: "SDK", 15 | fileName: "index", 16 | formats: ["es"] 17 | }, 18 | rollupOptions: { 19 | external: [ 20 | // Exclude all Node.js specific modules 21 | 'jsonwebtoken', 22 | 'ws', 23 | 'crypto', 24 | 'stream', 25 | 'util', 26 | 'buffer', 27 | 'fs', 28 | 'path', 29 | 'os' 30 | ], 31 | output: { 32 | entryFileNames: "index.js", 33 | format: "es" 34 | } 35 | } 36 | } 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /sdk/js/src/utils/base64.ts: -------------------------------------------------------------------------------- 1 | export function hexToUint8Array(hex: string): Uint8Array { 2 | if (hex.startsWith('0x')) { 3 | hex = hex.slice(2); 4 | } 5 | const length = hex.length / 2; 6 | const bytes = new Uint8Array(length); 7 | for (let i = 0; i < length; i++) { 8 | bytes[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16); 9 | } 10 | return bytes; 11 | } 12 | 13 | export function arrayBufferToBase64Url(buffer: any) { 14 | const bytes = new Uint8Array(buffer); 15 | let str = btoa(String.fromCharCode(...bytes)); 16 | return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 17 | } 18 | 19 | export function toBase64(buffer: Uint8Array): string { 20 | let binary = ""; 21 | for (let i = 0; i < buffer.length; i++) { 22 | binary += String.fromCharCode(buffer[i] as number); 23 | } 24 | return btoa(binary); 25 | } 26 | -------------------------------------------------------------------------------- /sdk/js/examples/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "virto-sdk-server-example", 3 | "version": "1.0.0", 4 | "description": "Example of using Virto SDK on a server", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc && tsc-alias", 8 | "start": "node dist/index.js", 9 | "dev": "ts-node-dev src/index.ts", 10 | "mongodb:start": "docker compose up -d mongodb", 11 | "dev:full": "npm run mongodb:start && sleep 5 && npm run dev" 12 | }, 13 | "dependencies": { 14 | "@types/jsonwebtoken": "^9.0.5", 15 | "cors": "^2.8.5", 16 | "express": "^4.18.2", 17 | "jsonwebtoken": "^9.0.2", 18 | "mongodb": "^6.16.0", 19 | "ts-node-dev": "^2.0.0", 20 | "tsc-alias": "^1.8.16" 21 | }, 22 | "devDependencies": { 23 | "@types/cors": "^2.8.13", 24 | "@types/express": "^4.17.17", 25 | "ts-node": "^10.9.1", 26 | "typescript": "^5.2.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/sube/sube-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "outDir": "./dist", 5 | "strict": true, 6 | "sourceMap": true, 7 | "lib": ["ESNext", "DOM"], 8 | "target": "esnext", 9 | "module": "NodeNext", 10 | "moduleResolution": "nodenext", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "esModuleInterop": true, 14 | "typeRoots": [ 15 | "node_modules/@types", 16 | "@types" 17 | ], 18 | "rootDir": "./src", 19 | "declaration": true, 20 | "useDefineForClassFields": true, 21 | "isolatedModules": true, 22 | "noUnusedLocals": true, 23 | "emitDeclarationOnly": false, 24 | "noUnusedParameters": true, 25 | "noImplicitReturns": true, 26 | "skipLibCheck": true, 27 | "forceConsistentCasingInFileNames": true, 28 | }, 29 | "include": ["src/"], 30 | "exclude": ["node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /lib/sube/examples/query_balance_ws.rs: -------------------------------------------------------------------------------- 1 | use env_logger; 2 | use serde::Deserialize; 3 | use sube::{sube, Error, ExtrinsicBody, Response, Result, SubeBuilder}; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub struct AccountInfo { 7 | pub nonce: u64, 8 | pub consumers: u64, 9 | pub providers: u64, 10 | pub sufficients: u64, 11 | pub data: AccountData, 12 | } 13 | 14 | #[derive(Debug, Deserialize)] 15 | pub struct AccountData { 16 | pub free: u128, 17 | pub reserved: u128, 18 | pub frozen: u128, 19 | pub flags: u128, 20 | } 21 | 22 | #[async_std::main] 23 | async fn main() -> Result<()> { 24 | let response = SubeBuilder::default() 25 | .with_url("wss://rococo-rpc.polkadot.io/system/account/0x3c85f79f28628bee75cdb9eddfeae249f813fad95f84120d068fbc990c4b717d") 26 | .await?; 27 | 28 | if let Response::Value(v) = response { 29 | println!("{}", v); 30 | } 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /lib/libwallet/examples/account_generation.rs: -------------------------------------------------------------------------------- 1 | use libwallet::{self, vault, Account}; 2 | use std::env; 3 | 4 | type Wallet = libwallet::Wallet>; 5 | 6 | #[async_std::main] 7 | async fn main() -> Result<(), Box> { 8 | let phrase = env::args().skip(1).collect::>().join(" "); 9 | 10 | let (vault, phrase) = if phrase.is_empty() { 11 | vault::Simple::generate_with_phrase(&mut rand_core::OsRng) 12 | } else { 13 | let phrase: libwallet::Mnemonic = phrase.parse().expect("Invalid phrase"); 14 | (vault::Simple::from_phrase(&phrase), phrase) 15 | }; 16 | 17 | let mut wallet = Wallet::new(vault); 18 | wallet.unlock(None, None).await.map_err(|_| format!("Failed to unlock vault"))?; 19 | let account = wallet.default_account().unwrap(); 20 | 21 | println!("Secret phrase: \"{phrase}\""); 22 | println!("Default Account: 0x{}", account); 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /components/globalStyles.js: -------------------------------------------------------------------------------- 1 | import { css } from "./utils.js"; 2 | 3 | export const globalStyles = await css` 4 | :host { 5 | --white: white; 6 | --whitesmoke: whitesmoke; 7 | --darkslategray: darkslategray; 8 | --lightgreen: lightgreen; 9 | --darkseagreen: darkseagreen; 10 | --black: #0000004D; 11 | /*From Figma*/ 12 | --green: #24AF37; 13 | --whitish-green: #c6ebc7; 14 | --dark-green: #006B0A; 15 | --grey-green: rgb(173, 190, 173); 16 | --extra-light-green: #DDFBE0; 17 | /*Dialog background*/ 18 | --gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.5) 100%), radial-gradient(84.04% 109.28% at 10.3% 12.14%, rgba(86, 201, 96, 0.5) 0%, rgba(198, 235, 199, 0) 98.5%); 19 | /*Fonts*/ 20 | --font-primary: Outfit, sans-serif; 21 | --font-secondary: Plus Jakarta, sans-serif; 22 | /* Unused by now*/ 23 | --color-accent-rgb: rgb(72, 61, 139); 24 | --color-text-alt: #446; 25 | } 26 | `; -------------------------------------------------------------------------------- /lib/sube/sube-js/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sube-js" 3 | version = "0.1.0" 4 | authors = ["david barinas "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | wasm-bindgen = "0.2.91" 12 | wee_alloc = { version = "0.4.5", optional = true } 13 | wasm-bindgen-futures = "0.4.41" 14 | serde = "1.0.152" 15 | serde_json = "1.0.91" 16 | serde-wasm-bindgen = "0.6.3" 17 | js-sys = "0.3.68" 18 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] } 19 | parity-scale-codec = "3.2.1" 20 | console_error_panic_hook = "0.1.7" 21 | # sp-core = "10.0.0" 22 | console_log = "1.0.0" 23 | log = "0.4" 24 | wasm-logger = "0.2" 25 | 26 | [dev-dependencies] 27 | console_error_panic_hook = { version = "0.1.6" } 28 | wasm-bindgen-test = "0.3.13" 29 | 30 | 31 | 32 | [dependencies.sube] 33 | path = ".." 34 | default-features=false 35 | features = [ 36 | "js" 37 | ] 38 | 39 | [features] 40 | default = ["alloc"] 41 | alloc = ["wee_alloc"] 42 | -------------------------------------------------------------------------------- /sdk/js/src/storage/IStorage.ts: -------------------------------------------------------------------------------- 1 | export interface IStorage { 2 | /** 3 | * Store a session with the given key 4 | * @param key - The unique identifier for the session 5 | * @param session - The session data to store 6 | */ 7 | store(key: string, session: T): Promise | void; 8 | 9 | /** 10 | * Retrieve a session by key 11 | * @param key - The unique identifier for the session 12 | * @returns The session data or null if not found 13 | */ 14 | get(key: string): Promise | T | null; 15 | 16 | /** 17 | * Get all stored sessions 18 | * @returns Array of all sessions 19 | */ 20 | getAll(): Promise | T[]; 21 | 22 | /** 23 | * Remove a session by key 24 | * @param key - The unique identifier for the session 25 | * @returns true if the session was removed, false if it didn't exist 26 | */ 27 | remove(key: string): Promise | boolean; 28 | 29 | /** 30 | * Clear all sessions 31 | */ 32 | clear(): Promise | void; 33 | } -------------------------------------------------------------------------------- /sdk/js/vite.config.node.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import wasm from 'vite-plugin-wasm'; 4 | 5 | export default defineConfig({ 6 | server: { 7 | port: 3000, 8 | }, 9 | plugins: [wasm()], 10 | build: { 11 | outDir: "dist/node", 12 | lib: { 13 | entry: resolve(__dirname, "src/index.node.ts"), 14 | name: "SDK", 15 | fileName: "index", 16 | formats: ["es", "cjs"] 17 | }, 18 | rollupOptions: { 19 | external: [ 20 | // Keep Node.js modules external but don't exclude them 21 | 'jsonwebtoken', 22 | 'ws', 23 | 'crypto', 24 | 'stream', 25 | 'util', 26 | 'buffer', 27 | 'fs', 28 | 'path', 29 | 'os' 30 | ], 31 | output: [ 32 | { 33 | format: "es", 34 | entryFileNames: "index.mjs" 35 | }, 36 | { 37 | format: "cjs", 38 | entryFileNames: "index.cjs" 39 | } 40 | ] 41 | } 42 | } 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /lib/libwallet/js/README.md: -------------------------------------------------------------------------------- 1 | # @virtonetwork/libwallet 2 | 3 | This library enables users to work with [`libwallet`][1] on Javascript, using WASM. 4 | 5 | ## Current status 6 | 7 | This library is a WORK IN PROGRESS, and so far, it's enabled to be used on Node environments. 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | import { Wallet } from '@virtonetwork/libwallet'; 13 | 14 | const wallet = new Wallet(); 15 | 16 | console.log(wallet.phrase); // -> "myself web subject call unfair return skull fatal radio spray insect fall twist ladder audit jump gravity modify search only blouse review receive south" 17 | console.log([...wallet.address]); // -> [ 108, 204, 206, 223, 179, 1, 220, 225, 205, 117, 149, 151, 188, 225, 113, 10, 136, 122, 112, 31, 72, 132, 118, 58, 116, 31, 226, 197, 27, 238, 54, 17 ] 18 | console.log(wallet.address.toHex()); // -> "0x6ccccedfb301dce1cd759597bce1710a887a701f4884763a741fe2c51bee3611" 19 | 20 | const sig = wallet.sign(Buffer.from("my message")); 21 | console.log(wallet.verify(Buffer.from("my message"), sig)); // -> true 22 | ``` 23 | 24 | [1]: https://github.com/virto-network/libwallet -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/src/sube.ts: -------------------------------------------------------------------------------- 1 | import { sube } from '@virtonetwork/sube'; 2 | import { JsWallet } from '@virtonetwork/libwallet'; 3 | 4 | export function setupSign(element: HTMLButtonElement) { 5 | let counter = 0 6 | const setCounter = async (count: number) => { 7 | 8 | const mnomic = document.querySelector('#mnomic')?.value ?? ''; 9 | const uri = document.querySelector('#uri')?.value ?? ''; 10 | const body = JSON.parse(document.querySelector('#data')?.value ?? ''); 11 | 12 | // await Initwallet(); 13 | 14 | const wallet = new JsWallet({ 15 | Simple: mnomic, 16 | }); 17 | 18 | await wallet.unlock("", null) 19 | 20 | console.log('from: ', wallet.getAddress().toHex()); 21 | 22 | await sube(uri, { 23 | body, 24 | from: wallet.getAddress().repr, 25 | sign: (message: Uint8Array) => wallet.sign(message), 26 | }); 27 | 28 | counter = count 29 | element.innerHTML = `Tx is ${counter}` 30 | } 31 | 32 | element.addEventListener('click', () => setCounter(counter + 1)); 33 | } 34 | -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css' 2 | import { setupSign } from './sube' 3 | 4 | document.querySelector('#app')!.innerHTML = ` 5 |
6 |
7 |

Sube Demo

8 |
9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 |
17 | 18 | 26 |
27 | 28 |
29 |
30 | ` 31 | 32 | setupSign(document.querySelector('#counter')!) 33 | -------------------------------------------------------------------------------- /lib/libwallet/js/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Pablo Dorado "] 3 | autoexamples = true 4 | edition = "2021" 5 | name = "libwallet-js" 6 | version = "0.1.0" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | libwallet = { path = "..", default-features = false } 13 | hex = { version = "0.4.3", optional = true } 14 | js-sys = "0.3.61" 15 | rand_core = { version = "0.6.3", features = ["getrandom"] } 16 | getrandom = { version = "0.2.1", features = ["js"] } 17 | serde = { version = "1.0.152", features = ["derive"] } 18 | serde-wasm-bindgen = "0.4.5" 19 | wasm-bindgen = "0.2.84" 20 | wasm-bindgen-futures = "0.4.34" 21 | wasm-logger = "0.2.0" 22 | console_error_panic_hook = "0.1.7" 23 | 24 | [dev-dependencies] 25 | wasm-bindgen-test = "0.3.34" 26 | 27 | # [target.'cfg(target_arch = "wasm32")'.dependencies.getrandom] 28 | # features = ["js"] 29 | 30 | [features] 31 | default = ["wallet", "js", "hex", "util_pin"] 32 | hex = ["dep:hex"] 33 | js = ["std"] 34 | std = [] 35 | util_pin = ["libwallet/util_pin"] 36 | vault_simple = ["libwallet/mnemonic", "libwallet/rand"] 37 | wallet = ["libwallet/serde", "libwallet/sr25519", "libwallet/substrate"] 38 | -------------------------------------------------------------------------------- /lib/sube/sube-js/LICENSE_MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 david barinas 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /lib/sube/sube-js/src/util.rs: -------------------------------------------------------------------------------- 1 | use sube::JsonValue; 2 | use wasm_bindgen::prelude::*; 3 | 4 | pub type Result = core::result::Result; 5 | #[wasm_bindgen] 6 | extern "C" { 7 | // Use `js_namespace` here to bind `console.log(..)` instead of just 8 | // `log(..)` 9 | #[wasm_bindgen(js_namespace = console)] 10 | fn log(s: &str); 11 | } 12 | 13 | pub fn decode_addresses(value: &JsonValue) -> JsonValue { 14 | log(format!("{:?}", value).as_str()); 15 | match value { 16 | JsonValue::Object(o) => o.iter().map(|(k, v)| (k, decode_addresses(v))).collect(), 17 | JsonValue::String(s) => { 18 | if s.starts_with("0x") { 19 | let input = s.as_str(); 20 | let decoded = hex::decode(&input[2..]) 21 | .expect("strings that start with 0x should be hex encoded") 22 | .into_iter() 23 | .map(|b| serde_json::json!(b)) 24 | .collect::>(); 25 | JsonValue::Array(decoded) 26 | } else { 27 | JsonValue::String(s.clone()) 28 | } 29 | } 30 | _ => value.clone(), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/sube/sube-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtonetwork/sube", 3 | "version": "1.0.0-alpha.2", 4 | "description": "fetch like API to interact with dotsama blockchains", 5 | "type": "module", 6 | "scripts": { 7 | "test": "node test/index.js", 8 | "build": "rm -rf dist pkg-web pkg-node && npm run build:pkg && npm run build:js", 9 | "build:pkg:node": "wasm-pack build --target bundler --out-dir ./dist/pkg", 10 | "build:pkg": "npm run build:pkg:node", 11 | "build:js": "npx concurrently \"npm:build:js:*\"", 12 | "build:js:esm": "tsc --module es2022 --target esnext --outDir dist/esm", 13 | "build:js:cjs": "tsc --module commonjs --target esnext --outDir dist/cjs" 14 | }, 15 | "files": [ 16 | "dist/" 17 | ], 18 | "main": "./dist/cjs/index.js", 19 | "browser": "./dist/esm/index.js", 20 | "types": "./dist/cjs/index.d.ts", 21 | "exports": { 22 | "import": "./dist/esm/index.js", 23 | "require": "./dist/cjs/index.js" 24 | }, 25 | "dependencies": { 26 | "sube-js": "file:./dist/pkg" 27 | }, 28 | "publishConfig": { 29 | "access": "public" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^22.13.1", 33 | "typescript": "^4.9.5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sdk/js/src/services/userService.ts: -------------------------------------------------------------------------------- 1 | export interface UserService { 2 | getUserAddress(username: string): Promise; 3 | } 4 | 5 | export class DefaultUserService implements UserService { 6 | constructor(private baseURL: string) {} 7 | 8 | async getUserAddress(username: string): Promise { 9 | try { 10 | const queryParams = new URLSearchParams({ 11 | userId: username, 12 | }); 13 | 14 | const res = await fetch(`${this.baseURL}/get-user-address?${queryParams}`, { 15 | method: "GET", 16 | headers: { "Content-Type": "application/json" }, 17 | }); 18 | 19 | if (!res.ok) { 20 | throw new Error(`HTTP error! status: ${res.status}`); 21 | } 22 | 23 | const response = await res.json(); 24 | 25 | if (!response.address) { 26 | throw new Error("User address not found in response"); 27 | } 28 | 29 | return response.address; 30 | } catch (error) { 31 | console.error("Failed to get user address:", error); 32 | throw new Error( 33 | `Failed to resolve username "${username}" to address: ${ 34 | error instanceof Error ? error.message : String(error) 35 | }` 36 | ); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /lib/sube/examples/query_identity.rs: -------------------------------------------------------------------------------- 1 | use core::future::{Future, IntoFuture}; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::{from_value, Value}; 4 | use sube::{sube, Response}; 5 | 6 | #[async_std::main] 7 | async fn main() -> sube::Result<()> { 8 | env_logger::init(); 9 | 10 | let result = sube!("ws://localhost:11004/identity/superOf/0x6d6f646c6b762f636d7479738501000000000000000000000000000000000000").await?; 11 | 12 | if let Response::Value(value) = result { 13 | let data = serde_json::to_value(&value).expect("to be serializable"); 14 | println!( 15 | "Account info: {}", 16 | serde_json::to_string_pretty(&data).expect("it must return an str") 17 | ); 18 | } 19 | 20 | let query = format!("ws://localhost:11004/identity/identityOf/0xbe6ed76ac48d5c7f1c5d2cab8a1d1e7a451dcc24b624b088ef554fd47ba21139"); 21 | 22 | let r = sube!(&query).await?; 23 | 24 | if let Response::Value(ref v) = r { 25 | let json_value = serde_json::to_value(v).expect("to be serializable"); 26 | println!("json: {:?}", json_value); 27 | let x = serde_json::to_string_pretty(&json_value).expect("it must return an str"); 28 | println!("Account info: {:?}", x); 29 | } 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /lib/sube/examples/send_tx_libwallet.rs: -------------------------------------------------------------------------------- 1 | use libwallet::{self, vault, Account, Signature}; 2 | use rand_core::OsRng; 3 | use serde_json::json; 4 | use std::{env, error::Error}; 5 | use sube::sube; 6 | 7 | type Wallet = libwallet::Wallet>; 8 | 9 | #[async_std::main] 10 | async fn main() -> Result<(), Box> { 11 | let phrase = env::args().skip(1).collect::>().join(" "); 12 | 13 | let (vault, phrase) = if phrase.is_empty() { 14 | vault::Simple::generate_with_phrase(&mut rand_core::OsRng) 15 | } else { 16 | let phrase: libwallet::Mnemonic = phrase.parse().expect("Invalid phrase"); 17 | (vault::Simple::from_phrase(&phrase), phrase) 18 | }; 19 | 20 | let mut wallet = Wallet::new(vault); 21 | wallet.unlock(None, None).await?; 22 | 23 | let account = wallet.default_account().unwrap(); 24 | 25 | let response = sube!( 26 | "wss://rococo-rpc.polkadot.io/balances/transfer" => 27 | (wallet, json!({ 28 | "dest": { 29 | "Id": account.public().as_ref(), 30 | }, 31 | "value": 100000 32 | })) 33 | ) 34 | .await.map_err(|_| format!("Error sending tx"))?; 35 | 36 | println!("Secret phrase: \"{phrase}\""); 37 | println!("Default Account: 0x{account}"); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/components.yml: -------------------------------------------------------------------------------- 1 | name: Test Components 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | registry-url: 'https://registry.npmjs.org/' 21 | 22 | - name: 🏗️ Install Dependencies 23 | run: npm ci 24 | working-directory: ./components 25 | 26 | - name: Install minimal Chrome dependencies 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get install -y --no-install-recommends \ 30 | libnss3 \ 31 | libatk1.0-0 \ 32 | libxss1 \ 33 | libxcomposite1 \ 34 | libxdamage1 \ 35 | libxrandr2 \ 36 | libgbm-dev \ 37 | libasound2t64 \ 38 | libgtk-3-0 \ 39 | libxfixes3 \ 40 | libxkbcommon-dev \ 41 | libdbus-1-3 \ 42 | fonts-liberation \ 43 | xdg-utils \ 44 | xvfb 45 | working-directory: ./components 46 | 47 | - name: 🧪 Run Web Test Runner Tests 48 | run: xvfb-run --auto-servernum npx web-test-runner 49 | working-directory: ./components 50 | -------------------------------------------------------------------------------- /sdk/js/src/storage/LocalStorageImpl.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "./IStorage"; 2 | 3 | export class LocalStorageImpl implements IStorage { 4 | private storageKey: string; 5 | 6 | constructor(storageKey: string = "sessions") { 7 | this.storageKey = storageKey; 8 | } 9 | 10 | store(key: string, session: T): void { 11 | const sessions = this.getAllFromStorage(); 12 | sessions[key] = session; 13 | localStorage.setItem(this.storageKey, JSON.stringify(sessions)); 14 | } 15 | 16 | get(key: string): T | null { 17 | const sessions = this.getAllFromStorage(); 18 | return sessions[key] || null; 19 | } 20 | 21 | getAll(): T[] { 22 | const sessions = this.getAllFromStorage(); 23 | return Object.values(sessions); 24 | } 25 | 26 | remove(key: string): boolean { 27 | const sessions = this.getAllFromStorage(); 28 | if (key in sessions) { 29 | delete sessions[key]; 30 | localStorage.setItem(this.storageKey, JSON.stringify(sessions)); 31 | return true; 32 | } 33 | return false; 34 | } 35 | 36 | clear(): void { 37 | localStorage.removeItem(this.storageKey); 38 | } 39 | 40 | private getAllFromStorage(): Record { 41 | const saved = localStorage.getItem(this.storageKey); 42 | return saved ? JSON.parse(saved) : {}; 43 | } 44 | } -------------------------------------------------------------------------------- /sdk/js/src/index.web.ts: -------------------------------------------------------------------------------- 1 | import SDK from "./sdk"; 2 | import Auth from "./auth"; 3 | import Transfer from "./transfer"; 4 | import System from "./system"; 5 | import Utility from "./utility"; 6 | import CustomModule from "./custom"; 7 | import TransactionQueue from "./transactionQueue"; 8 | import TransactionExecutor from "./transactionExecutor"; 9 | import { DefaultUserService } from "./services/userService"; 10 | 11 | export type { 12 | TransferOptions, 13 | TransferByUsernameOptions, 14 | SendAllOptions, 15 | SendAllByUsernameOptions, 16 | BalanceInfo, 17 | UserInfo, 18 | } from "./transfer"; 19 | 20 | export type { 21 | RemarkOptions, 22 | } from "./system"; 23 | 24 | export type { 25 | BatchOptions, 26 | } from "./utility"; 27 | 28 | export type { 29 | TransactionStatus, 30 | TransactionMetadata, 31 | TransactionEventType, 32 | TransactionEvent, 33 | TransactionEventCallback, 34 | } from "./transactionQueue"; 35 | 36 | export type { 37 | SDKOptions, 38 | TransactionConfirmationLevel, 39 | TransactionResult, 40 | AttestationData, 41 | PreparedRegistrationData, 42 | } from "./types"; 43 | 44 | export type { 45 | UserService, 46 | } from "./services/userService"; 47 | 48 | export { 49 | SDK, 50 | Auth, 51 | Transfer, 52 | System, 53 | Utility, 54 | CustomModule, 55 | TransactionQueue, 56 | TransactionExecutor, 57 | DefaultUserService, 58 | }; 59 | 60 | export default SDK; 61 | 62 | -------------------------------------------------------------------------------- /lib/scales/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "scale-serialization" 3 | description = "SCALE Serialization" 4 | version = "1.0.0-beta2" 5 | authors = ["Daniel Olano "] 6 | edition = "2018" 7 | repository = "https://github.com/virto-network/scales" 8 | license = "Apache-2.0" 9 | 10 | [dependencies] 11 | bytes = { version = "1.1.0", default-features = false } 12 | scale-info = { version = "2.10.0", default-features = false, features = ["serde"] } 13 | serde = { version = "1.0.137", default-features = false } 14 | serde_json = { version = "1.0.80", default-features = false, optional = true } 15 | codec = { version = "3.1.2", package = "parity-scale-codec", default-features = false, optional = true } 16 | hex = { version = "0.4.3", default-features = false, features = ["alloc"], optional = true } 17 | log = "0.4.17" 18 | 19 | 20 | 21 | [features] 22 | default = ["std", "codec", "json", "hex", "experimental-serializer"] 23 | std = ["scale-info/std", "bytes/std"] 24 | experimental-serializer = [] 25 | json = ["serde_json/preserve_order"] 26 | 27 | [dev-dependencies] 28 | anyhow = "1.0.57" 29 | codec = { version = "3.1.2", package = "parity-scale-codec", features = ["derive"] } 30 | scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } 31 | serde = { version = "1.0.137", default-features = false, features = ["derive"] } 32 | serde_json = { version = "1.0.80", default-features = false, features = ["alloc"] } 33 | -------------------------------------------------------------------------------- /lib/scales/README.md: -------------------------------------------------------------------------------- 1 | # scales - SCALE Serialization 2 | 3 | Making use of [type information](https://github.com/paritytech/scale-info) this library allows 4 | conversion to/from [SCALE](https://github.com/paritytech/parity-scale-codec) encoded data, 5 | specially useful when conversion is for dynamic types like JSON. 6 | 7 | ### From SCALE 8 | 9 | `scales::Value` wraps the raw SCALE binary data and the type id within type registry 10 | giving you an object that can be serialized to any compatible format. 11 | 12 | ```rust 13 | let value = scales::Value::new(scale_binary_data, type_id, &type_registry); 14 | serde_json::to_string(value)?; 15 | ``` 16 | 17 | ### To SCALE 18 | 19 | Public methods from the `scales::serializer::*` module(feature `experimental-serializer`) 20 | allow for a best effort conversion of dynamic types(e.g. `serde_json::Value`) to SCALE 21 | binary format. The serializer tries to be smart when interpreting the input and convert it 22 | to the desired format dictated by the provided type in the registry. 23 | 24 | ```rust 25 | // simple conversion 26 | let scale_data = to_vec(some_serializable_input); // or to_bytes(&mut bytes, input); 27 | 28 | // with type info 29 | let scale_data = to_vec_with_info(input, Some((®istry, type_id))); 30 | 31 | // from an unordered list of properties that make an object 32 | let input = vec![("prop_b", 123), ("prop_a", 456)]; 33 | let scale_data = to_vec_from_iter(input, (®istry, type_id)); 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /sdk/js/src/index.ts: -------------------------------------------------------------------------------- 1 | import SDK from "./sdk"; 2 | import Auth from "./auth"; 3 | import Transfer from "./transfer"; 4 | import System from "./system"; 5 | import Utility from "./utility"; 6 | import CustomModule from "./custom"; 7 | import TransactionQueue from "./transactionQueue"; 8 | import TransactionExecutor from "./transactionExecutor"; 9 | import { DefaultUserService } from "./services/userService"; 10 | import ServerSDK from "./serverSdk"; 11 | 12 | export type { 13 | TransferOptions, 14 | TransferByUsernameOptions, 15 | SendAllOptions, 16 | SendAllByUsernameOptions, 17 | BalanceInfo, 18 | UserInfo, 19 | } from "./transfer"; 20 | 21 | export type { 22 | RemarkOptions, 23 | } from "./system"; 24 | 25 | export type { 26 | BatchOptions, 27 | } from "./utility"; 28 | 29 | export type { 30 | TransactionStatus, 31 | TransactionMetadata, 32 | TransactionEventType, 33 | TransactionEvent, 34 | TransactionEventCallback, 35 | } from "./transactionQueue"; 36 | 37 | export type { 38 | SDKOptions, 39 | TransactionConfirmationLevel, 40 | TransactionResult, 41 | AttestationData, 42 | PreparedRegistrationData, 43 | } from "./types"; 44 | 45 | export type { 46 | UserService, 47 | } from "./services/userService"; 48 | 49 | export { 50 | SDK, 51 | ServerSDK, 52 | Auth, 53 | Transfer, 54 | System, 55 | Utility, 56 | CustomModule, 57 | TransactionQueue, 58 | TransactionExecutor, 59 | DefaultUserService, 60 | }; 61 | 62 | export default SDK; -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/sdk-pr.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | paths: ['sdk/js/**'] 4 | 5 | name: sdk 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: extractions/setup-just@v2 13 | 14 | - name: Check SDK 15 | working-directory: sdk/js 16 | run: just check 17 | 18 | test: 19 | runs-on: ubuntu-latest 20 | needs: check 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: extractions/setup-just@v2 24 | 25 | - name: Install Puppeteer dependencies 26 | run: | 27 | apt-get update && apt-get install -y \ 28 | libnss3 \ 29 | libatk-bridge2.0-0 \ 30 | libatk1.0-0 \ 31 | libcups2 \ 32 | libdrm2 \ 33 | libxcomposite1 \ 34 | libxdamage1 \ 35 | libxrandr2 \ 36 | libgbm1 \ 37 | libasound2 \ 38 | libpangocairo-1.0-0 \ 39 | libx11-xcb1 \ 40 | libgtk-3-0 \ 41 | libxshmfence1 \ 42 | libxfixes3 \ 43 | libxext6 \ 44 | libxcb1 \ 45 | libx11-6 \ 46 | libxss1 \ 47 | lsb-release \ 48 | wget \ 49 | fonts-liberation \ 50 | libappindicator3-1 \ 51 | libu2f-udev \ 52 | libvulkan1 53 | 54 | - name: Test SDK 55 | working-directory: sdk/js 56 | run: just test 57 | -------------------------------------------------------------------------------- /lib/sube/src/signer.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use core::{future::Future, marker::PhantomData}; 3 | 4 | pub type Bytes = [u8; N]; 5 | 6 | /// Signed extrinsics need to be signed by a `Signer` before submission 7 | pub trait Signer { 8 | type Account: AsRef<[u8]>; 9 | type Signature: AsRef<[u8]>; 10 | 11 | fn sign(&self, data: impl AsRef<[u8]>) -> impl Future>; 12 | 13 | fn account(&self) -> Self::Account; 14 | } 15 | 16 | /// Wrapper to create a standard signer from an account and closure 17 | pub struct SignerFn { 18 | account: Bytes<32>, 19 | signer: S, 20 | _fut: PhantomData, 21 | } 22 | 23 | impl Signer for SignerFn 24 | where 25 | S: Fn(&[u8]) -> SF, 26 | SF: Future>>, 27 | { 28 | type Account = Bytes<32>; 29 | type Signature = Bytes<64>; 30 | 31 | fn sign(&self, data: impl AsRef<[u8]>) -> impl Future> { 32 | (self.signer)(data.as_ref()) 33 | } 34 | 35 | fn account(&self) -> Self::Account { 36 | self.account 37 | } 38 | } 39 | 40 | impl, S, SF> From<(A, S)> for SignerFn 41 | where 42 | A: AsRef<[u8]>, 43 | S: Fn(&[u8]) -> SF, 44 | { 45 | fn from((account, signer): (A, S)) -> Self { 46 | SignerFn { 47 | account: account.as_ref().try_into().expect("32bit account"), 48 | signer, 49 | _fut: PhantomData, 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sdk/js/demo/README.md: -------------------------------------------------------------------------------- 1 | # Virto SDK Demo 2 | 3 | This demo showcases the Virto SDK with different storage options for session management. 4 | 5 | ## Features 6 | 7 | - **Multiple Storage Options**: Choose between localStorage, IndexedDB, and sessionStorage 8 | - **Session Management**: Register users, connect, and sign transactions 9 | 10 | ## Storage Options 11 | 12 | ### localStorage 13 | - **Persistence**: Data persists until manually cleared 14 | 15 | ### IndexedDB 16 | - **Persistence**: Data persists until manually cleared 17 | 18 | ### sessionStorage 19 | - **Persistence**: Data cleared when tab closes 20 | 21 | ## Getting Started 22 | 23 | 1. **Install dependencies**: 24 | ```bash 25 | npm install 26 | ``` 27 | 28 | 2. **Start the development environment**: 29 | ```bash 30 | npm run dev 31 | ``` 32 | 33 | 3. **Open your browser** and navigate to the provided URL (usually `http://localhost:3017`) 34 | 35 | 1. **Register User**: Fill in user details and click "Register User" 36 | 2. **Connect**: Click "Connect" to establish a connection 37 | 3. **Sign Transaction**: Enter a command and click "Sign" to test transaction signing 38 | 39 | ## Troubleshooting 40 | 41 | ### Storage Issues 42 | - **IndexedDB not working**: Check if your browser supports IndexedDB 43 | - **Data not persisting**: Verify storage type selection 44 | - **Quota exceeded**: Clear storage or use IndexedDB for larger capacity 45 | 46 | ### SDK Issues 47 | - **Registration fails**: Check console for detailed error messages 48 | - **Connection fails**: Ensure the provider URL is accessible 49 | -------------------------------------------------------------------------------- /components/example.minimal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Login 12 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /sdk/js/src/index.node.ts: -------------------------------------------------------------------------------- 1 | import SDK from "./sdk"; 2 | import Auth from "./auth"; 3 | import Transfer from "./transfer"; 4 | import System from "./system"; 5 | import Utility from "./utility"; 6 | import CustomModule from "./custom"; 7 | import TransactionQueue from "./transactionQueue"; 8 | import TransactionExecutor from "./transactionExecutor"; 9 | import { DefaultUserService } from "./services/userService"; 10 | import ServerSDK from "./serverSdk"; 11 | import ServerAuth from "./serverAuth"; 12 | 13 | export type { 14 | TransferOptions, 15 | TransferByUsernameOptions, 16 | SendAllOptions, 17 | SendAllByUsernameOptions, 18 | BalanceInfo, 19 | UserInfo, 20 | } from "./transfer"; 21 | 22 | export type { 23 | RemarkOptions, 24 | } from "./system"; 25 | 26 | export type { 27 | BatchOptions, 28 | } from "./utility"; 29 | 30 | export type { 31 | TransactionStatus, 32 | TransactionMetadata, 33 | TransactionEventType, 34 | TransactionEvent, 35 | TransactionEventCallback, 36 | } from "./transactionQueue"; 37 | 38 | export type { 39 | SDKOptions, 40 | ServerSDKOptions, 41 | TransactionConfirmationLevel, 42 | TransactionResult, 43 | AttestationData, 44 | PreparedRegistrationData, 45 | PreparedConnectionData, 46 | JWTPayload, 47 | } from "./types"; 48 | 49 | export type { 50 | UserService, 51 | } from "./services/userService"; 52 | 53 | export { 54 | SDK, 55 | ServerSDK, 56 | ServerAuth, 57 | Auth, 58 | Transfer, 59 | System, 60 | Utility, 61 | CustomModule, 62 | TransactionQueue, 63 | TransactionExecutor, 64 | DefaultUserService, 65 | }; 66 | 67 | export default SDK; 68 | 69 | -------------------------------------------------------------------------------- /lib/sube/examples/pallet_communities.rs: -------------------------------------------------------------------------------- 1 | use env_logger; 2 | use futures_util::future::join_all; 3 | use serde_json; 4 | use sube::{sube, ExtrinsicBody, Response, Result, SubeBuilder}; 5 | 6 | 7 | #[async_std::main] 8 | async fn main() -> Result<()> { 9 | env_logger::init(); 10 | 11 | let response = sube!("wss://kreivo.io/communityMemberships/account/0xe25b1e3758a5fbedb956b36113252f9e866d3ece688364cc9d34eb01f4b2125d/2").await.expect("to work"); 12 | 13 | if let Response::ValueSet(value) = response { 14 | let data = serde_json::to_value(&value).expect("to be serializable"); 15 | println!( 16 | "Collection Array {}", 17 | serde_json::to_string_pretty(&data).expect("it must return an str") 18 | ); 19 | } 20 | 21 | let response = sube!("ws://127.0.0.1:12281/communityMemberships/collection").await?; 22 | 23 | if let Response::ValueSet(value) = response { 24 | let data = serde_json::to_value(&value).expect("to be serializable"); 25 | println!( 26 | "Collection {}", 27 | serde_json::to_string_pretty(&data).expect("it must return an str") 28 | ); 29 | } 30 | 31 | let result = sube!("https://kreivo.io/system/account/0x12840f0626ac847d41089c4e05cf0719c5698af1e3bb87b66542de70b2de4b2b").await?; 32 | 33 | if let Response::Value(value) = result { 34 | let data = serde_json::to_value(&value).expect("to be serializable"); 35 | println!( 36 | "Account info: {}", 37 | serde_json::to_string_pretty(&data).expect("it must return an str") 38 | ); 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /sdk/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "ES2020", 7 | "DOM" 8 | ], 9 | "rootDir": "./", 10 | "rootDirs": [ 11 | "./src", 12 | "./js" 13 | ], 14 | "strict": true, 15 | "noImplicitAny": true, 16 | "strictNullChecks": true, 17 | "strictFunctionTypes": true, 18 | "strictBindCallApply": true, 19 | "strictPropertyInitialization": true, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noUncheckedIndexedAccess": true, 27 | "moduleResolution": "node", 28 | "baseUrl": "./", 29 | "paths": { 30 | "@/*": [ 31 | "src/*" 32 | ], 33 | "@virtonetwork/sdk/descriptors": [ 34 | ".papi/descriptors/dist/index.d.ts" 35 | ] 36 | }, 37 | "esModuleInterop": true, 38 | "allowSyntheticDefaultImports": true, 39 | "resolveJsonModule": true, 40 | "sourceMap": true, 41 | "typeRoots": [ 42 | "./node_modules/@types" 43 | ], 44 | "types": [ 45 | "jest", 46 | "jest-environment-puppeteer" 47 | ], 48 | "declaration": true, 49 | "declarationMap": true, 50 | "skipLibCheck": true, 51 | "forceConsistentCasingInFileNames": true, 52 | "allowJs": true, 53 | "checkJs": true 54 | }, 55 | "include": [ 56 | "types", 57 | "src/**/*", 58 | "test/**/*", 59 | "global.d.ts", 60 | "**/*.test.ts", 61 | "**/*.spec.ts" 62 | ], 63 | "exclude": [ 64 | "node_modules", 65 | "dist" 66 | ], 67 | "ts-node": { 68 | "transpileOnly": true 69 | } 70 | } -------------------------------------------------------------------------------- /lib/sube/sube-js/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtonetwork/sube", 3 | "version": "1.0.0-alpha.2", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@virtonetwork/sube", 9 | "version": "1.0.0-alpha.2", 10 | "dependencies": { 11 | "sube-js": "file:./dist/pkg" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.13.1", 15 | "typescript": "^4.9.5" 16 | } 17 | }, 18 | "dist/pkg": { 19 | "name": "sube-js", 20 | "version": "0.1.0" 21 | }, 22 | "node_modules/@types/node": { 23 | "version": "22.13.1", 24 | "dev": true, 25 | "license": "MIT", 26 | "dependencies": { 27 | "undici-types": "~6.20.0" 28 | } 29 | }, 30 | "node_modules/sube-js": { 31 | "resolved": "dist/pkg", 32 | "link": true 33 | }, 34 | "node_modules/typescript": { 35 | "version": "4.9.5", 36 | "dev": true, 37 | "license": "Apache-2.0", 38 | "bin": { 39 | "tsc": "bin/tsc", 40 | "tsserver": "bin/tsserver" 41 | }, 42 | "engines": { 43 | "node": ">=4.2.0" 44 | } 45 | }, 46 | "node_modules/undici-types": { 47 | "version": "6.20.0", 48 | "dev": true, 49 | "license": "MIT" 50 | } 51 | }, 52 | "dependencies": { 53 | "@types/node": { 54 | "version": "22.13.1", 55 | "dev": true, 56 | "requires": { 57 | "undici-types": "~6.20.0" 58 | } 59 | }, 60 | "sube-js": { 61 | "version": "file:dist/pkg" 62 | }, 63 | "typescript": { 64 | "version": "4.9.5", 65 | "dev": true 66 | }, 67 | "undici-types": { 68 | "version": "6.20.0", 69 | "dev": true 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sdk/js/src/serverSdk.ts: -------------------------------------------------------------------------------- 1 | import { getWsProvider } from "polkadot-api/ws-provider/node"; 2 | import ServerAuth from "./serverAuth"; 3 | import { ServerSDKOptions } from "./types"; 4 | import { createClient } from "polkadot-api"; 5 | import { InMemoryImpl, IStorage, SerializableSignerData } from "./storage"; 6 | 7 | /** 8 | * Server version of the SDK 9 | * This class only contains components that do NOT depend on browser APIs 10 | * and can be executed in a Node.js environment 11 | */ 12 | export default class ServerSDK { 13 | private _auth: ServerAuth; 14 | private _client: any = null; 15 | private _provider: any = null; 16 | 17 | /** 18 | * Creates a new ServerSDK instance 19 | * 20 | * @param options - Configuration options for the server SDK 21 | * @param storage - Optional storage implementation for sessions 22 | */ 23 | constructor( 24 | options: ServerSDKOptions, 25 | storage?: IStorage, 26 | ) { 27 | 28 | this._provider = getWsProvider(options.provider_url); 29 | this._client = createClient(this._provider); 30 | 31 | const getClient = async () => { 32 | return this._client; 33 | }; 34 | 35 | 36 | if (!storage) { 37 | storage = new InMemoryImpl(); 38 | } 39 | 40 | // Create ServerAuth with JWT configuration 41 | this._auth = new ServerAuth( 42 | options.federate_server, 43 | getClient, 44 | storage, 45 | { 46 | secret: options.config.jwt.secret, 47 | expiresIn: options.config.jwt.expiresIn 48 | } 49 | ); 50 | } 51 | 52 | public get auth() { 53 | return this._auth; 54 | } 55 | } -------------------------------------------------------------------------------- /lib/libwallet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Daniel Olano "] 3 | autoexamples = true 4 | edition = "2018" 5 | name = "libwallet" 6 | version = "0.3.0" 7 | 8 | [dependencies] 9 | # base dependencies 10 | arrayvec = {version = "0.7.2", default-features = false} 11 | 12 | serde = {version = "1.0", default-features = false, features = ["derive"], optional = true} 13 | 14 | # feature util_pin 15 | hmac = {version = "0.12.1", default-features = false, optional = true} 16 | pbkdf2 = {version = "0.11.0", default-features = false, optional = true} 17 | sha2 = {version = "0.10.2", default-features = false, optional = true} 18 | 19 | mnemonic = {package = "bip0039", version = "0.10.1", default-features = false, optional = true} 20 | 21 | rand_core = {version = "0.6.3", optional = true} 22 | # substrate related 23 | schnorrkel = {version = "0.11.4", default-features = false, optional = true}# soft derivation in no_std 24 | rand_chacha = {version = "0.3.1", default-features = false, optional = true} 25 | 26 | # vault os 27 | keyring = {version = "1.1.2", optional = true} 28 | # vault pass 29 | prs-lib = {version = "0.2.1", optional = true} 30 | log = { version = "0.4" } 31 | 32 | [dev-dependencies] 33 | async-std = {version = "1.10.0", features = ["attributes"]} 34 | serde_json = {version = "1.0", default-features = false, features = ["alloc"]} 35 | # pass vault example 36 | dirs = "4.0" 37 | 38 | 39 | [features] 40 | default = ["substrate"] 41 | # default = ["std", "substrate", "vault_simple", "mnemonic", "rand", "vault_pass", "vault_os", "util_pin"] 42 | rand = ["rand_core", "schnorrkel?/getrandom"] 43 | sr25519 = ["dep:schnorrkel"] 44 | std = [ 45 | "rand_core/getrandom", 46 | ] 47 | substrate = ["sr25519"] 48 | util_pin = ["pbkdf2", "hmac", "sha2"] 49 | vault_os = ["keyring"] 50 | vault_pass = ["prs-lib"] 51 | vault_simple = ["mnemonic", "rand"] 52 | 53 | [workspace] 54 | members = [ 55 | "js", 56 | ] 57 | -------------------------------------------------------------------------------- /lib/sube/examples/send_tx_builder.rs: -------------------------------------------------------------------------------- 1 | use libwallet::{self, vault, Account, Signature}; 2 | use serde_json::json; 3 | use std::{env, error::Error}; 4 | use sube::{Bytes, SubeBuilder, Signer}; 5 | type Wallet = libwallet::Wallet>; 6 | 7 | #[async_std::main] 8 | async fn main() -> Result<(), Box> { 9 | let phrase = env::args().skip(1).collect::>().join(" "); 10 | 11 | let (vault, phrase) = if phrase.is_empty() { 12 | vault::Simple::generate_with_phrase(&mut rand_core::OsRng) 13 | } else { 14 | let phrase: libwallet::Mnemonic = phrase.parse().expect("Invalid phrase"); 15 | (vault::Simple::from_phrase(&phrase), phrase) 16 | }; 17 | 18 | let mut wallet = Wallet::new(vault); 19 | wallet.unlock(None, None).await?; 20 | 21 | let account = wallet.default_account().unwrap(); 22 | 23 | let signer = sube::SignerFn::from(( 24 | account.public().as_ref(), 25 | |message: &[u8]| { 26 | let message = message.to_vec(); 27 | let wallet = &wallet; 28 | 29 | async move { 30 | let result = wallet 31 | .sign(&message) 32 | .await 33 | .map(|signature| signature.as_ref().try_into().unwrap()) 34 | .map_err(|_| sube::Error::Signing)?; 35 | 36 | Ok::, sube::Error>(result) 37 | } 38 | }, 39 | )); 40 | 41 | let response = SubeBuilder::default() 42 | .with_url("wss://rococo-rpc.polkadot.io/balances/transfer") 43 | .with_body(json!({ 44 | "dest": { 45 | "Id": account.public().as_ref() 46 | }, 47 | "value": 100000 48 | })) 49 | .with_signer(signer) 50 | .await.map_err(|_| format!("Failed to send tx")); 51 | 52 | println!("{:?}", response); 53 | Ok(()) 54 | } 55 | 56 | -------------------------------------------------------------------------------- /sdk/js/demo/src/storage/SessionStorageImpl.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "../../../src/storage"; 2 | 3 | /** 4 | * Example sessionStorage implementation for client-side session storage 5 | * Data will be cleared when the browser tab is closed 6 | * 7 | * Usage: 8 | * ```typescript 9 | * import { SDK } from "@virto-sdk/js"; 10 | * import { SessionStorageImpl } from "./storage/SessionStorageImpl"; 11 | * 12 | * const sessionStorage = new SessionStorageImpl('virto-sessions'); 13 | * 14 | * const sdk = new SDK({ 15 | * storage: sessionStorage 16 | * }); 17 | * ``` 18 | */ 19 | export class SessionStorageImpl implements IStorage { 20 | private storageKey: string; 21 | 22 | constructor(storageKey: string = "virto-sessions") { 23 | this.storageKey = storageKey; 24 | } 25 | 26 | store(key: string, session: T): void { 27 | const sessions = this.getAllFromStorage(); 28 | sessions[key] = session; 29 | sessionStorage.setItem(this.storageKey, JSON.stringify(sessions)); 30 | } 31 | 32 | get(key: string): T | null { 33 | const sessions = this.getAllFromStorage(); 34 | return sessions[key] || null; 35 | } 36 | 37 | getAll(): T[] { 38 | const sessions = this.getAllFromStorage(); 39 | return Object.values(sessions); 40 | } 41 | 42 | remove(key: string): boolean { 43 | const sessions = this.getAllFromStorage(); 44 | if (key in sessions) { 45 | delete sessions[key]; 46 | sessionStorage.setItem(this.storageKey, JSON.stringify(sessions)); 47 | return true; 48 | } 49 | return false; 50 | } 51 | 52 | clear(): void { 53 | sessionStorage.removeItem(this.storageKey); 54 | } 55 | 56 | private getAllFromStorage(): Record { 57 | const saved = sessionStorage.getItem(this.storageKey); 58 | return saved ? JSON.parse(saved) : {}; 59 | } 60 | } -------------------------------------------------------------------------------- /.github/workflows/libwallet-publish.yml: -------------------------------------------------------------------------------- 1 | name: Libwallet Publish 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ['lib/libwallet/**'] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | if: "${{ !startsWith(github.event.head_commit.message, 'chore: bump version') }}" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 20 17 | registry-url: 'https://registry.npmjs.org/' 18 | 19 | - name: Ensure access to npmjs 20 | run: npm whoami --registry https://registry.npmjs.org/ 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | 24 | - name: Config git user 25 | run: | 26 | git config --global user.name "${{ github.actor }}" 27 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 28 | 29 | - name: Install Rust 30 | run: | 31 | curl https://sh.rustup.rs -sSf | sh -s -- -y 32 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 33 | source "$HOME/.cargo/env" 34 | 35 | - name: Install dependencies 36 | working-directory: lib/libwallet/js 37 | run: npm ci 38 | 39 | - name: Install wasm-pack 40 | run: | 41 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 42 | 43 | - name: Build 44 | working-directory: lib/libwallet/js 45 | run: npm run build 46 | 47 | - name: Publish 48 | working-directory: lib/libwallet/js 49 | run: | 50 | VERSION_OUTPUT=$(npm version patch -m "chore: bump version to %s") 51 | 52 | echo "New version: $VERSION_OUTPUT" 53 | 54 | git add package.json package-lock.json 55 | git commit -m "chore: bump version to $VERSION_OUTPUT" 56 | git push origin main 57 | 58 | npm publish --access public 59 | env: 60 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /.github/workflows/sube-publish.yml: -------------------------------------------------------------------------------- 1 | name: Sube Publish 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ['lib/sube/**'] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | if: "${{ !startsWith(github.event.head_commit.message, 'chore: bump version') }}" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 20 17 | registry-url: 'https://registry.npmjs.org/' 18 | 19 | - name: Ensure access to npmjs 20 | run: npm whoami --registry https://registry.npmjs.org/ 21 | env: 22 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 23 | 24 | - name: Config git user 25 | run: | 26 | git config --global user.name "${{ github.actor }}" 27 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 28 | 29 | - name: Install Rust 30 | run: | 31 | curl https://sh.rustup.rs -sSf | sh -s -- -y 32 | echo "$HOME/.cargo/bin" >> $GITHUB_PATH 33 | source "$HOME/.cargo/env" 34 | 35 | - name: Install dependencies 36 | working-directory: lib/sube/sube-js 37 | run: npm ci 38 | 39 | - name: Install wasm-pack 40 | run: | 41 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 42 | 43 | - name: Build 44 | working-directory: lib/sube/sube-js 45 | run: npm run build 46 | 47 | - name: Publish 48 | working-directory: lib/sube/sube-js 49 | run: | 50 | VERSION_OUTPUT=$(npm version prerelease --preid=alpha -m "chore: bump version to %s") 51 | 52 | echo "New version: $VERSION_OUTPUT" 53 | 54 | git add package.json package-lock.json 55 | git commit -m "chore: bump version to $VERSION_OUTPUT" 56 | git push origin main 57 | 58 | npm publish --access public 59 | env: 60 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | -------------------------------------------------------------------------------- /vos-programs/memberships/src/main.rs: -------------------------------------------------------------------------------- 1 | #[vos::bin] 2 | mod memberships { 3 | type UserId = [u8; 32]; 4 | type MembershipId = [u8; 32]; 5 | type Role = String; 6 | 7 | #[derive(Default)] 8 | struct Membership { 9 | reputation: u16, 10 | } 11 | 12 | #[derive(Debug)] 13 | enum Error { 14 | WrongPermissions, 15 | MembershipNotOwnedByUser, 16 | } 17 | 18 | #[vos(storage)] 19 | #[derive(Default)] 20 | pub struct Memberships { 21 | memberships: Map, 22 | user_memberships: Map, 23 | roles: Map>, 24 | update_roles: Set, 25 | } 26 | 27 | impl Memberships { 28 | #[vos(action)] 29 | pub fn get_membership(&self, user: UserId) -> Option { 30 | self.user_memberships 31 | .get(user) 32 | .and_then(|m| self.memberships.get(m)) 33 | } 34 | 35 | #[vos(action)] 36 | pub fn get_user_roles(&self, user: UserId) -> Option> { 37 | let membership = self.get_membership(user)?; 38 | self.roles.get(membership.id) 39 | } 40 | 41 | #[vos(action)] 42 | pub fn update_reputation( 43 | &mut self, 44 | membership: MembershipId, 45 | reputation: u16, 46 | ) -> Result<(), Error> { 47 | if !self.can_update_memberships(self.env().caller()) { 48 | return Err(Error::WrongPermissions); 49 | } 50 | let Some(membership) = self.memberships.get_mut(membership) else { 51 | return Err(Error::MembershipNotOwnedByUser); 52 | }; 53 | membership.reputation = reputation; 54 | Ok(()) 55 | } 56 | } 57 | 58 | // internal usage 59 | impl Memberships { 60 | fn can_update_membership(&self, user: &Caller) -> bool { 61 | self.update_roles.intersects(user.roles()) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sdk/js/src/types.ts: -------------------------------------------------------------------------------- 1 | export type BaseProfile = { 2 | id: string; 3 | name?: string; 4 | }; 5 | 6 | export type User = { 7 | profile: Profile; 8 | }; 9 | 10 | export type Command = { 11 | url: string; 12 | body: any; 13 | hex: string; 14 | }; 15 | 16 | export interface TransactionResult { 17 | ok: boolean; 18 | hash?: string; 19 | error?: string; 20 | } 21 | 22 | export interface AttestationData { 23 | authenticator_data: string; 24 | client_data: string; 25 | public_key: string; 26 | meta: { 27 | deviceId: string; 28 | context: number; 29 | authority_id: string; 30 | } 31 | } 32 | 33 | export interface PreparedRegistrationData { 34 | attestation: AttestationData; 35 | hashedUserId: string; 36 | credentialId: string; 37 | userId: string; 38 | passAccountAddress: string; 39 | } 40 | 41 | export interface ServerSDKOptions { 42 | federate_server: string; 43 | provider_url: string; 44 | config: { 45 | jwt: { 46 | secret: string; 47 | expiresIn?: string; 48 | } 49 | }; 50 | } 51 | 52 | export interface PreparedConnectionData { 53 | userId: string; 54 | assertionResponse: { 55 | id: string; 56 | rawId: string; 57 | type: string; 58 | response: { 59 | authenticatorData: string; 60 | clientDataJSON: string; 61 | signature: string; 62 | } 63 | }; 64 | blockNumber: number; 65 | } 66 | 67 | export interface JWTPayload { 68 | userId: string; 69 | publicKey: string; 70 | address: string; 71 | exp: number; 72 | iat: number; 73 | } 74 | 75 | export type { 76 | TransactionStatus, 77 | TransactionMetadata, 78 | TransactionEvent, 79 | TransactionEventCallback, 80 | TransactionEventType, 81 | } from './transactionQueue'; 82 | 83 | export type TransactionConfirmationLevel = 'submitted' | 'included' | 'finalized'; 84 | 85 | export interface SDKOptions { 86 | federate_server: string; 87 | provider_url: string; 88 | confirmation_level?: TransactionConfirmationLevel; 89 | onProviderStatusChange?: (status: any) => void; 90 | } 91 | 92 | export type SignFn = (input: Uint8Array) => Promise | Uint8Array; 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VirtoSDK 2 | 3 | VirtoSDK is a comprehensive suite of tools designed by Virto to empower organizations with scalable, customizable solutions tailored to their specific needs. Our monorepo encompasses both low-level utilities and high-level components, ensuring that you can find exactly what you need, whether it's for blockchain interaction or seamless web integration. 4 | 5 | ## Overview 6 | 7 | VirtoSDK offers a versatile collection of tools and libraries catering to various use cases: 8 | 9 | ### Low-Level Tools 10 | 11 | 1. **Client Libraries**: 12 | - Seamlessly connect to Substrate blockchains. 13 | - Create and manage wallets with ease. 14 | 15 | 2. **Wink! APIs**: 16 | - Deploy small WASM+WASI programs as backend services on VirtoOS (VOS). 17 | - Utilize a wide range of functionalities, including: 18 | - Authentication 19 | - Payments 20 | - Marketplaces 21 | - Memberships 22 | - Administration and Governance 23 | 24 | ### High-Level Components 25 | 26 | 1. **Web Components**: 27 | Easily integrate with existing web applications. 28 | 29 | - **Virto-Connect**: Provides a straightforward interface for connecting to an organization’s authentication system, interacting with the blockchain, and sending feeless transactions such as payments. 30 | 31 | ## The Virto stack 32 | ![architecture](https://github.com/user-attachments/assets/1fa810b9-eb07-4f08-beca-05c160662767) 33 | 34 | 35 | ## Use Cases 36 | 37 | Whether you're looking to: 38 | 39 | - Develop custom backend services using Wink! APIs 40 | - Enhance your web applications with our high-level components 41 | - Simplify user onboarding with seamless authentication solutions 42 | - Enable secure, efficient payment systems without transaction fees 43 | 44 | VirtoSDK is designed to meet these needs and more. 45 | 46 | ## Getting Started 47 | 48 | To get started with VirtoSDK: 49 | 50 | 1. **Explore the Monorepo**: Discover tools and libraries that match your use case. 51 | 2. **Integrate Web Components**: Use our high-level components for quick integration into your web applications. 52 | 3. **Deploy Wink! APIs**: Set up backend services on VOS to enhance your organization’s capabilities. 53 | -------------------------------------------------------------------------------- /lib/sube/cli/src/opts.rs: -------------------------------------------------------------------------------- 1 | use crate::Result; 2 | use async_std::path::PathBuf; 3 | use std::str::FromStr; 4 | use structopt::StructOpt; 5 | 6 | /// SUBmit Extrinsics and query chain data 7 | #[derive(StructOpt, Debug)] 8 | #[structopt(name = "sube")] 9 | pub(crate) struct Opt { 10 | /// Address of the chain to connect to. Http protocol assumed if not provided. 11 | /// 12 | /// When the metadata option is provided but not the chain, only offline functionality is 13 | /// supported 14 | #[structopt(short, long, default_value = "localhost")] 15 | pub chain: String, 16 | /// Format for the output (json,json-pretty,scale,hex) 17 | #[structopt(short, long, default_value = "json")] 18 | pub output: Output, 19 | /// Use existing metadata from the filesystem(in SCALE format) 20 | #[structopt(short, long)] 21 | pub metadata: Option, 22 | #[structopt(short, long)] 23 | pub quiet: bool, 24 | #[structopt(short, long, parse(from_occurrences))] 25 | pub verbose: usize, 26 | 27 | #[structopt(value_name = "QUERY/CALL")] 28 | pub input: String, 29 | } 30 | 31 | #[derive(Debug)] 32 | pub(crate) enum Output { 33 | Json(bool), 34 | Scale, 35 | Hex, 36 | } 37 | 38 | impl Output { 39 | pub(crate) fn format(&self, out: O) -> Result> 40 | where 41 | O: serde::Serialize + Into>, 42 | { 43 | Ok(match self { 44 | Output::Json(pretty) => { 45 | if *pretty { 46 | serde_json::to_vec_pretty(&out)? 47 | } else { 48 | serde_json::to_vec(&out)? 49 | } 50 | } 51 | Output::Scale => out.into(), 52 | Output::Hex => format!("0x{}", hex::encode(out.into())).into(), 53 | }) 54 | } 55 | } 56 | 57 | impl FromStr for Output { 58 | type Err = std::convert::Infallible; 59 | fn from_str(s: &str) -> Result { 60 | Ok(match s { 61 | "json" => Output::Json(false), 62 | "json-pretty" => Output::Json(true), 63 | "scale" => Output::Scale, 64 | "hex" => Output::Hex, 65 | _ => Output::Json(false), 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/libwallet/js/test/wallet.test.js: -------------------------------------------------------------------------------- 1 | import { ok, throws, deepEqual } from "node:assert"; 2 | import { describe, it } from "node:test"; 3 | import { Wallet } from "../src/lib.js"; 4 | 5 | describe("Wallet", () => { 6 | const phrase = 7 | "myself web subject call unfair return skull fatal radio spray insect fall twist ladder audit jump gravity modify search only blouse review receive south"; 8 | 9 | describe("constructor", () => { 10 | it("initializes a wallet using a randomly-generated seed", () => { 11 | throws(() => new Wallet({})); 12 | 13 | new Wallet({ Simple: undefined }); 14 | new Wallet({ Simple: null }); 15 | }); 16 | 17 | it("initializes a wallet using a given seed", () => { 18 | new Wallet({ Simple: phrase }); 19 | }); 20 | }); 21 | 22 | describe(".address", () => { 23 | it("fails to retrieve if wallet is unlocked", () => { 24 | const wallet = new Wallet({ Simple: phrase }); 25 | throws(() => wallet.address); 26 | }); 27 | 28 | it("unlocks and retrieves an address", async () => { 29 | const wallet = new Wallet({ Simple: phrase }); 30 | await wallet.unlock(); 31 | 32 | deepEqual( 33 | [...wallet.address], 34 | [ 35 | 108, 204, 206, 223, 179, 1, 220, 225, 205, 117, 149, 151, 188, 225, 36 | 113, 10, 136, 122, 112, 31, 72, 132, 118, 58, 116, 31, 226, 197, 27, 37 | 238, 54, 17, 38 | ] 39 | ); 40 | }); 41 | 42 | it("when available, retrieves the public address as hex string", async () => { 43 | const wallet = new Wallet({ Simple: phrase }); 44 | await wallet.unlock(); 45 | 46 | deepEqual( 47 | wallet.address.toHex(), 48 | "0x6ccccedfb301dce1cd759597bce1710a887a701f4884763a741fe2c51bee3611" 49 | ); 50 | }); 51 | }); 52 | 53 | describe("sign/verify", () => { 54 | it("sign and verify a known message", async () => { 55 | const message = Buffer.from( 56 | "This message should be signed and verified against the obtained signature" 57 | ); 58 | 59 | const wallet = new Wallet({ 60 | Simple: phrase, 61 | }); 62 | await wallet.unlock(); 63 | 64 | const sig = wallet.sign(message); 65 | ok(wallet.verify(message, sig)); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /.github/workflows/sdk-publish.yml: -------------------------------------------------------------------------------- 1 | name: SDK Publish 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: ['sdk/**'] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | if: "${{ !startsWith(github.event.head_commit.message, 'chore: bump version') }}" 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 20 17 | registry-url: 'https://registry.npmjs.org/' 18 | 19 | - name: Clean package-lock.json (if exists) 20 | working-directory: sdk/js 21 | run: | 22 | if [ -f package-lock.json ]; then 23 | if npm run | grep -q 'clean'; then npm run clean; fi 24 | fi 25 | 26 | - name: Cache node modules 27 | uses: actions/cache@v3 28 | env: 29 | cache-name: cache-node-modules 30 | with: 31 | path: sdk/js/node_modules 32 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('sdk/js/package.json') }} 33 | restore-keys: | 34 | ${{ runner.os }}-build-${{ env.cache-name }}- 35 | ${{ runner.os }}-build- 36 | ${{ runner.os }}- 37 | 38 | - name: Ensure access to npmjs 39 | run: npm whoami --registry https://registry.npmjs.org/ 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | 43 | - name: Config git user 44 | run: | 45 | git config --global user.name "${{ github.actor }}" 46 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 47 | 48 | - name: Install dependencies 49 | working-directory: sdk/js 50 | run: npm ci 51 | 52 | - name: Build 53 | working-directory: sdk/js 54 | run: npm run build 55 | 56 | - name: Publish SDK 57 | working-directory: sdk/js 58 | run: | 59 | VERSION_OUTPUT=$(npm version prerelease --preid=alpha -m "chore: bump version to %s") 60 | 61 | echo "New version: $VERSION_OUTPUT" 62 | 63 | git add package.json package-lock.json 64 | git commit -m "chore: bump version to $VERSION_OUTPUT" 65 | git push origin main 66 | 67 | npm publish --access public 68 | env: 69 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 70 | -------------------------------------------------------------------------------- /sdk/js/src/custom.ts: -------------------------------------------------------------------------------- 1 | import { Binary } from "polkadot-api"; 2 | import { kreivo } from "@virtonetwork/sdk/descriptors"; 3 | import TransactionQueue from "./transactionQueue"; 4 | import { TransactionResult } from "./types"; 5 | 6 | export interface CustomTxOptions { 7 | callDataHex: string; 8 | } 9 | 10 | export default class CustomModule { 11 | constructor( 12 | private readonly clientFactory?: () => Promise, 13 | private readonly transactionQueue?: TransactionQueue 14 | ) {} 15 | 16 | private async getClient(): Promise { 17 | if (!this.clientFactory) { 18 | throw new Error("Client factory not configured"); 19 | } 20 | return await this.clientFactory(); 21 | } 22 | 23 | /** 24 | * Submit a custom transaction call (WAITS FOR INCLUSION) 25 | * Returns when transaction is included in block 26 | * 27 | * @param sessionSigner - Optional session signer for faster transactions 28 | * @param options - Custom transaction options including call data 29 | * @returns Promise with transaction result including hash 30 | * 31 | * @example 32 | * const result = await custom.submitCallAsync(auth.sessionSigner, { 33 | * callDataHex: "0x..." 34 | * }); 35 | * console.log("Transaction included:", result.hash); 36 | */ 37 | async submitCallAsync(sessionSigner: any | null, options: CustomTxOptions): Promise { 38 | if (!this.transactionQueue) { 39 | throw new Error("TransactionQueue not configured"); 40 | } 41 | 42 | try { 43 | if (!sessionSigner) { 44 | throw new Error("Session signer is required for submitCallAsync"); 45 | } 46 | 47 | const client = await this.getClient(); 48 | const kreivoApi = client.getTypedApi(kreivo); 49 | const transaction = await kreivoApi.txFromCallData(Binary.fromHex(options.callDataHex)); 50 | 51 | const transactionId = await this.transactionQueue.addTransaction( 52 | transaction, 53 | sessionSigner 54 | ); 55 | 56 | const result = await this.transactionQueue.executeTransaction(transactionId, sessionSigner); 57 | 58 | return { 59 | ok: result.included, 60 | hash: result.hash, 61 | error: result.error 62 | }; 63 | } catch (error) { 64 | return { 65 | ok: false, 66 | error: error instanceof Error ? error.message : String(error) 67 | }; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/sube/src/http.rs: -------------------------------------------------------------------------------- 1 | use crate::prelude::*; 2 | use crate::rpc::{self, Rpc, RpcResult}; 3 | use core::{convert::TryInto, fmt}; 4 | use jsonrpc::{ 5 | error::{standard_error, StandardError}, 6 | serde_json::value::to_raw_value, 7 | }; 8 | use reqwest::Client; 9 | use serde::Deserialize; 10 | pub use url::Url; 11 | 12 | #[derive(Debug)] 13 | pub struct Backend(Url); 14 | 15 | impl Backend { 16 | pub fn new(url: U) -> Self 17 | where 18 | U: TryInto, 19 | >::Error: fmt::Debug, 20 | { 21 | Backend(url.try_into().expect("Url")) 22 | } 23 | } 24 | 25 | impl Rpc for Backend { 26 | /// HTTP based JSONRpc request expecting an hex encoded result 27 | async fn rpc(&self, method: &str, params: &[&str]) -> RpcResult 28 | where 29 | T: for<'de> Deserialize<'de>, 30 | { 31 | log::info!("RPC `{}` to {}", method, &self.0); 32 | 33 | let res = Client::new() 34 | .post(self.0.to_string()) 35 | .json(&rpc::Request { 36 | id: 1.into(), 37 | jsonrpc: Some("2.0"), 38 | method, 39 | params: &Self::convert_params(params), 40 | }) 41 | .send() 42 | .await 43 | .map_err(|err| rpc::error::Error::Transport(Box::new(err)))?; 44 | 45 | let status = res.status(); 46 | let res = if status.is_success() { 47 | res.json::() 48 | .await 49 | .map_err(|err| { 50 | standard_error( 51 | StandardError::ParseError, 52 | Some(to_raw_value(&err.to_string()).unwrap()), 53 | ) 54 | })? 55 | .result::()? 56 | } else { 57 | log::debug!("RPC HTTP status: {}", res.status()); 58 | let err = res 59 | .text() 60 | .await 61 | .unwrap_or_else(|_| status.canonical_reason().expect("to have a message").into()); 62 | 63 | let err = to_raw_value(&err).expect("error string"); 64 | 65 | return Err(if status.is_client_error() { 66 | standard_error(StandardError::InvalidRequest, Some(err)).into() 67 | } else { 68 | standard_error(StandardError::InternalError, Some(err)).into() 69 | }); 70 | }; 71 | 72 | Ok(res) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/sube/src/hasher.rs: -------------------------------------------------------------------------------- 1 | use crate::meta_ext::Hasher; 2 | use crate::prelude::*; 3 | use blake2::{ 4 | digest::{ 5 | typenum::{U16, U32}, 6 | Output, 7 | }, 8 | Blake2b, Digest, 9 | }; 10 | use core::hash::Hasher as _; 11 | 12 | /// hashes and encodes the provided input with the specified hasher 13 | pub fn hash>(hasher: &Hasher, input: I) -> Vec { 14 | let mut input = input.as_ref(); 15 | // input might be a hex encoded string 16 | let mut data = vec![]; 17 | if input.starts_with(b"0x") { 18 | data.append(&mut hex::decode(&input[2..]).expect("hex string")); 19 | input = data.as_ref(); 20 | }; 21 | 22 | #[inline] 23 | fn digest(input: &[u8]) -> Output { 24 | let mut hasher = T::new(); 25 | hasher.update(input); 26 | hasher.finalize() 27 | } 28 | 29 | match hasher { 30 | Hasher::Blake2_128 => digest::>(input).to_vec(), 31 | Hasher::Blake2_256 => digest::>(input).to_vec(), 32 | Hasher::Blake2_128Concat => [digest::>(input).as_slice(), input].concat(), 33 | Hasher::Twox128 => twox_hash(input), 34 | Hasher::Twox256 => unimplemented!(), 35 | Hasher::Twox64Concat => twox_hash_concat(input), 36 | Hasher::Identity => input.into(), 37 | } 38 | } 39 | 40 | fn twox_hash_concat(input: &[u8]) -> Vec { 41 | let mut dest = [0; 8]; 42 | let mut h = twox_hash::XxHash64::with_seed(0); 43 | 44 | h.write(input); 45 | let r = h.finish(); 46 | dest.copy_from_slice(&r.to_le_bytes()); 47 | [dest.as_ref(), input].concat() 48 | } 49 | 50 | fn twox_hash(input: &[u8]) -> Vec { 51 | let mut dest: [u8; 16] = [0; 16]; 52 | 53 | let mut h0 = twox_hash::XxHash64::with_seed(0); 54 | let mut h1 = twox_hash::XxHash64::with_seed(1); 55 | h0.write(input); 56 | h1.write(input); 57 | let r0 = h0.finish(); 58 | let r1 = h1.finish(); 59 | 60 | let (first, last) = dest.split_at_mut(8); 61 | first.copy_from_slice(&r0.to_le_bytes()); 62 | last.copy_from_slice(&r1.to_le_bytes()); 63 | dest.into() 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::*; 69 | use hex_literal::hex; 70 | 71 | #[test] 72 | fn hash_blake_hex() { 73 | let out1 = hash(&Hasher::Blake2_128, "0x68656c6c6f"); 74 | let out2 = hash(&Hasher::Blake2_128, hex!("68656c6c6f")); 75 | assert_eq!(out1, out2,); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/sube/sube-js/demo/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | #app { 41 | max-width: 1280px; 42 | margin: 0 auto; 43 | padding: 2rem; 44 | text-align: center; 45 | } 46 | 47 | .logo { 48 | height: 6em; 49 | padding: 1.5em; 50 | will-change: filter; 51 | } 52 | .logo:hover { 53 | filter: drop-shadow(0 0 2em #646cffaa); 54 | } 55 | .logo.vanilla:hover { 56 | filter: drop-shadow(0 0 2em #3178c6aa); 57 | } 58 | 59 | .card { 60 | /* padding: 2em; */ 61 | /* max-width: 80%; */ 62 | display: flex; 63 | flex-direction: column; 64 | } 65 | 66 | .card > .row { 67 | display: flex; 68 | flex-direction: row; 69 | justify-content: space-between; 70 | padding: .2rem 0rem; 71 | } 72 | 73 | .row > label { 74 | margin-right: 20px; 75 | width: calc(30% - 20px); 76 | text-align: left; 77 | } 78 | .row > input { 79 | width: 70%; 80 | } 81 | 82 | .read-the-docs { 83 | color: #888; 84 | } 85 | 86 | button { 87 | border-radius: 8px; 88 | border: 1px solid transparent; 89 | padding: 0.6em 1.2em; 90 | font-size: 1em; 91 | font-weight: 500; 92 | font-family: inherit; 93 | background-color: #1a1a1a; 94 | cursor: pointer; 95 | transition: border-color 0.25s; 96 | } 97 | button:hover { 98 | border-color: #646cff; 99 | } 100 | button:focus, 101 | button:focus-visible { 102 | outline: 4px auto -webkit-focus-ring-color; 103 | } 104 | input { 105 | all: unset; 106 | border: 2px solid; 107 | border-color: rgba(#646cff, 0.1); 108 | border-radius: 5px; 109 | } 110 | 111 | input:focus { 112 | border-radius: 5px; 113 | border: 2px solid; 114 | border-color: #646cff; 115 | outline: none; 116 | } 117 | -------------------------------------------------------------------------------- /sdk/js/src/membership.ts: -------------------------------------------------------------------------------- 1 | export default class Membership { 2 | constructor( 3 | private readonly baseUrl: string, 4 | ) {} 5 | 6 | /** 7 | * Delivers/grants an existing membership to a user for DAO participation 8 | * This method delivers a membership to the specified user, enabling them to become 9 | * a member of the decentralized autonomous organization (DAO) and participate in its governance. 10 | * The membership must already exist and be available for delivery. 11 | * 12 | * @param userId - The unique identifier of the user who will receive the membership 13 | * @returns Promise resolving to the server response containing membership delivery details 14 | * @throws Will throw an error if the membership delivery fails or if there's a server error 15 | */ 16 | public async addOne(userId: string) { 17 | const res = await fetch(`${this.baseUrl}/add-member`, { 18 | method: "POST", 19 | headers: { "Content-Type": "application/json" }, 20 | body: JSON.stringify({ userId }), 21 | }); 22 | 23 | const data = await res.json(); 24 | console.log("Add member response:", data); 25 | 26 | if (!res.ok || data.statusCode >= 500) { 27 | const errorMessage = data.message || `Server error: ${res.status} ${res.statusText}`; 28 | throw new Error(`Failed to add member: ${errorMessage}`); 29 | } 30 | 31 | return data; 32 | } 33 | 34 | /** 35 | * Checks if a user is a member of the DAO 36 | * This method verifies whether the specified address corresponds to a current member 37 | * of the decentralized autonomous organization (DAO). It queries the server to determine 38 | * the membership status of the given address. 39 | * 40 | * @param address - The blockchain address to check for membership status 41 | * @returns Promise resolving to a boolean indicating whether the address is a member 42 | * @throws Will throw an error if the membership check fails or if there's a server error 43 | */ 44 | public async isMember(address: string) { 45 | const res = await fetch(`${this.baseUrl}/is-member?address=${address}`, { 46 | method: "GET", 47 | headers: { "Content-Type": "application/json" }, 48 | }); 49 | 50 | const data = await res.json(); 51 | console.log("Is member response:", data); 52 | 53 | if (!res.ok || data.statusCode >= 500) { 54 | const errorMessage = data.message || `Server error: ${res.status} ${res.statusText}`; 55 | throw new Error(`Failed to check member status: ${errorMessage}`); 56 | } 57 | 58 | return data.ok; 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /lib/sube/cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use async_std::{ 3 | io::{self, ReadExt, WriteExt}, 4 | path::PathBuf, 5 | task::block_on, 6 | }; 7 | use codec::Decode; 8 | use opts::Opt; 9 | use structopt::StructOpt; 10 | use sube::{sube, Backend, Metadata}; 11 | use url::Url; 12 | 13 | mod opts; 14 | 15 | async fn run() -> Result<()> { 16 | let opt = Opt::from_args(); 17 | 18 | stderrlog::new() 19 | .verbosity(opt.verbose) 20 | .quiet(opt.quiet) 21 | .init() 22 | .unwrap(); 23 | 24 | let url = chain_string_to_url(&opt.chain)?; 25 | 26 | // let backend = sube::ws::Backend::new_ws2(url.as_str()).await?; 27 | let backend = sube::http::Backend::new(url.as_str()); 28 | 29 | let meta = if let Some(m) = opt.metadata { 30 | get_meta_from_fs(&m) 31 | .await 32 | .ok_or_else(|| anyhow!("Couldn't read Metadata from file"))? 33 | } else { 34 | backend.metadata().await? 35 | }; 36 | 37 | let res = sube::(backend, &meta, &opt.input, None, |_, _| {}).await?; 38 | 39 | io::stdout().write_all(&opt.output.format(res)?).await?; 40 | writeln!(io::stdout()).await?; 41 | Ok(()) 42 | } 43 | 44 | fn main() { 45 | block_on(async { 46 | match run().await { 47 | Ok(_) => {} 48 | Err(err) => { 49 | log::error!("{}", err); 50 | std::process::exit(1); 51 | } 52 | } 53 | }) 54 | } 55 | 56 | // Function that tries to be "smart" about what the user might want to actually connect to 57 | fn chain_string_to_url(chain: &str) -> Result { 58 | let chain = if !chain.starts_with("ws://") 59 | && !chain.starts_with("wss://") 60 | && !chain.starts_with("http://") 61 | && !chain.starts_with("https://") 62 | { 63 | ["wss", &chain].join("://") 64 | } else { 65 | chain.into() 66 | }; 67 | 68 | let mut url = Url::parse(&chain)?; 69 | 70 | if url.host_str().eq(&Some("localhost")) && url.port().is_none() { 71 | const WS_PORT: u16 = 9944; 72 | const HTTP_PORT: u16 = 9933; 73 | let port = match url.scheme() { 74 | "ws" => WS_PORT, 75 | _ => HTTP_PORT, 76 | }; 77 | 78 | url.set_port(Some(port)).expect("known port"); 79 | } 80 | 81 | Ok(url) 82 | } 83 | 84 | async fn get_meta_from_fs(path: &PathBuf) -> Option { 85 | let mut m = Vec::new(); 86 | let mut f = async_std::fs::File::open(path).await.ok()?; 87 | f.read_to_end(&mut m).await.ok()?; 88 | Metadata::decode(&mut m.as_slice()).ok() 89 | } 90 | -------------------------------------------------------------------------------- /lib/sube/README.md: -------------------------------------------------------------------------------- 1 | # Sube 2 | 3 | A client library for Substrate chains, doing less by design than [subxt](https://github.com/paritytech/substrate-subxt) with a big focus on size and portability so it can run in constrainted environments like the browser. 4 | 5 | Making use of the type information in a chain's metadata(`>= v15`) and powered by our [Scales](../scales/) library, Sube allows automatic conversion between the [SCALE](https://github.com/paritytech/parity-scale-codec) binary format used by the blockchain with a human-readable representation like JSON without having to hardcode type information for each network. 6 | When submitting extrinsics Sube only does that, it's your responsability to sign the payload with a different tool first(e.g. [libwallet](../libwallet)) before you feed the extrinsic data to the library. 7 | 8 | Sube supports multiple backends under different feature flags like `http`, `http-web` or `ws`/`wss`. 9 | 10 | 11 | ## Example Usage 12 | 13 | To make Queries/Extrinsics using Sube, you can use the `SubeBuilder` or the convenient `sube!` macro. [here are the examples](./examples/) 14 | 15 | 16 | ## Progressive decentralization 17 | 18 | > 🛠️ ⚠️ [Upcoming feature](https://github.com/virto-network/sube/milestone/2) 19 | 20 | The true _raison d'etre_ of Sube is not to create yet another Substrate client but to enable the Virto.Network and any project in the ecosystem to reach a broader audience of end-users and developers by lowering the technical entry barrier and drastically improving the overall user experience of interacting with blockchains. We call it **progressive decentralization**. 21 | 22 | When paired with our plugin runtime [Valor](https://github.com/virto-network/valor), Sube can be exposed as an HTTP API that runs both in the server and the browser and be composed with other plugins to create higher level APIs that a client aplication can use from any plattform thanks to the ubiquitousness of HTTP. 23 | We imagine existing centralized projects easily integrating with Substrate blockchains in the server with the option to progressively migrate to a decentralized set-up with whole backends later running in the user device(web browser included). 24 | 25 | But progressive decentralization goes beyond the migration of a centralized project, it's rather about giving users the the best experience by possibly combining the best of both worlds. A Sube powered application can start being served from a server to have an immediate response and 0 start-up time and since plugins can be hot-swapped, the blockchain backend can be switched from HTTP to lightnode transparently without the application code ever realizing, giving our users with bad connectivity and slower devices the opportunity to enjoy the best possible user experience without compromizing decentralization. 26 | 27 | -------------------------------------------------------------------------------- /sdk/js/demo/src/storage/IndexedDBStorage.ts: -------------------------------------------------------------------------------- 1 | import { openDB, IDBPDatabase } from 'idb'; 2 | import { IStorage } from "../../../src/storage"; 3 | 4 | /** 5 | * Example IndexedDB implementation for client-side session storage using the 'idb' package 6 | * 7 | * Usage: 8 | * ```typescript 9 | * import { SDK } from "@virto-sdk/js"; 10 | * import { IndexedDBStorage } from "./storage/IndexedDBStorage"; 11 | * 12 | * const indexedDBStorage = new IndexedDBStorage('VirtoSessions', 'sessions'); 13 | * 14 | * const sdk = new SDK({ 15 | * storage: indexedDBStorage 16 | * }); 17 | * ``` 18 | */ 19 | export class IndexedDBStorage implements IStorage { 20 | private dbName: string; 21 | private storeName: string; 22 | private dbConnection: IDBPDatabase | null = null; 23 | 24 | constructor(dbName: string = 'VirtoSessions', storeName: string = 'sessions') { 25 | this.dbName = dbName; 26 | this.storeName = storeName; 27 | } 28 | 29 | private async getDB(): Promise { 30 | if (!this.dbConnection) { 31 | const storeName = this.storeName; 32 | this.dbConnection = await openDB(this.dbName, 1, { 33 | upgrade(db) { 34 | if (!db.objectStoreNames.contains(storeName)) { 35 | db.createObjectStore(storeName, { keyPath: 'id' }); 36 | } 37 | }, 38 | }); 39 | } 40 | return this.dbConnection; 41 | } 42 | 43 | async store(key: string, session: T): Promise { 44 | const db = await this.getDB(); 45 | await db.put(this.storeName, { id: key, data: session }); 46 | } 47 | 48 | async get(key: string): Promise { 49 | const db = await this.getDB(); 50 | const result = await db.get(this.storeName, key); 51 | return result ? result.data : null; 52 | } 53 | 54 | async getAll(): Promise { 55 | const db = await this.getDB(); 56 | const results = await db.getAll(this.storeName); 57 | return results.map((item: any) => item.data); 58 | } 59 | 60 | async remove(key: string): Promise { 61 | const db = await this.getDB(); 62 | const existing = await db.get(this.storeName, key); 63 | 64 | if (existing) { 65 | await db.delete(this.storeName, key); 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | async clear(): Promise { 72 | const db = await this.getDB(); 73 | await db.clear(this.storeName); 74 | } 75 | 76 | async close(): Promise { 77 | if (this.dbConnection) { 78 | this.dbConnection.close(); 79 | this.dbConnection = null; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /components/virto-notification/notification.js: -------------------------------------------------------------------------------- 1 | import { html, css } from "../utils.js"; 2 | import { globalStyles } from "../globalStyles.js"; 3 | 4 | const notificationTp = html` 5 | 6 | 7 | 8 | `; 9 | 10 | const notificationCss = await css` 11 | :host { 12 | display: block; 13 | margin: 1em 0; 14 | } 15 | 16 | wa-callout { 17 | font-family: Outfit, sans-serif; 18 | padding: var(--spacing, 1em); 19 | border-radius: 8px; 20 | background-color: var(--extra-light-green); 21 | color: var(--darkslategray); 22 | transition: all 300ms ease; 23 | } 24 | 25 | /* Variant Styles */ 26 | :host([variant="brand"]) wa-callout { 27 | background-color: var(--green); 28 | color: var(--whitesmoke); 29 | } 30 | 31 | :host([variant="success"]) wa-callout { 32 | background-color: var(--lightgreen); 33 | color: var(--darkslategray); 34 | } 35 | 36 | :host([variant="warning"]) wa-callout { 37 | background-color: #ffcc00; /* Example warning color */ 38 | color: var(--darkslategray); 39 | } 40 | 41 | :host([variant="danger"]) wa-callout { 42 | background-color: #ff3333; /* Example danger color */ 43 | color: var(--whitesmoke); 44 | } 45 | 46 | :host([variant="neutral"]) wa-callout { 47 | background-color: var(--grey-green); 48 | color: var(--darkslategray); 49 | } 50 | 51 | /* Appearance Adjustments */ 52 | :host([appearance="plain"]) wa-callout { 53 | border: none; 54 | } 55 | 56 | :host([appearance="accent"]) wa-callout { 57 | border: 2px solid var(--green); 58 | } 59 | 60 | /* Size Adjustments */ 61 | :host([size="small"]) wa-callout { 62 | padding: 0.5em; 63 | font-size: 0.875em; 64 | } 65 | 66 | :host([size="large"]) wa-callout { 67 | padding: 1.5em; 68 | font-size: 1.25em; 69 | } 70 | 71 | /* Icon Styling */ 72 | wa-icon { 73 | --icon-size: 1.5em; 74 | --icon-color: inherit; 75 | } 76 | 77 | wa-callout:hover { 78 | opacity: 0.9; 79 | } 80 | `; 81 | 82 | export class NotificationVirto extends HTMLElement { 83 | static TAG = "virto-notification"; 84 | // Aparenlty there's no need to add the attributes on the array observedAttributes, unless we want to do something with them on the code. Ive deleted them by now in order to check that the tests pass anyway. 85 | 86 | constructor() { 87 | super(); 88 | this.attachShadow({ mode: "open" }); 89 | this.shadowRoot.appendChild(notificationTp.content.cloneNode(true)); 90 | this.shadowRoot.adoptedStyleSheets = [globalStyles, notificationCss]; 91 | 92 | this.callout = this.shadowRoot.querySelector("wa-callout"); 93 | } 94 | 95 | } 96 | 97 | if (!customElements.get(NotificationVirto.TAG)) { 98 | customElements.define(NotificationVirto.TAG, NotificationVirto); 99 | } -------------------------------------------------------------------------------- /components/avatar.js: -------------------------------------------------------------------------------- 1 | import { html, css } from "./utils.js"; 2 | import { globalStyles } from "./globalStyles.js"; 3 | 4 | const avatarTp = html` 5 | 6 | 7 | 8 | `; 9 | 10 | const avatarCss = await css` 11 | :host { 12 | display: inline-block; 13 | } 14 | 15 | wa-avatar { 16 | --background-color: var(--extra-light-green); 17 | --text-color: var(--darkslategray); 18 | --size: 48px; 19 | font-family: var(--font-primary); 20 | --border-radius: 50px; 21 | border-radius: var(--border-radius); 22 | transition: transform 0.2s ease-out, background-color 0.2s ease-out; 23 | } 24 | 25 | wa-avatar::part(base) { 26 | transition: all 0.2s ease-out; 27 | } 28 | 29 | wa-avatar::part(icon) { 30 | color: var(--green); 31 | } 32 | 33 | wa-avatar::part(initials) { 34 | font-weight: 600; 35 | } 36 | 37 | wa-avatar::part(image) { 38 | object-fit: cover; 39 | } 40 | 41 | :host([shape="rounded-square"]) wa-avatar { 42 | --border-radius: 12px; 43 | border-radius: var(--border-radius); 44 | } 45 | 46 | :host(:hover) wa-avatar:hover { 47 | transform: scale(1.1); 48 | --background-color: var(--whitish-green); 49 | } 50 | `; 51 | 52 | export class AvatarVirto extends HTMLElement { 53 | static TAG = "virto-avatar"; 54 | 55 | static observedAttributes = ["image", "label", "initials", "loading", "shape"]; 56 | 57 | constructor() { 58 | super(); 59 | this.attachShadow({ mode: "open" }); 60 | this.shadowRoot.appendChild(avatarTp.content.cloneNode(true)); 61 | this.shadowRoot.adoptedStyleSheets = [globalStyles, avatarCss]; 62 | 63 | this.waAvatar = this.shadowRoot.querySelector("wa-avatar"); 64 | this.waAvatar.addEventListener("wa-error", this.#handleError); 65 | this.updateAvatar(); 66 | } 67 | 68 | attributeChangedCallback(name, oldValue, newValue) { 69 | if (oldValue === newValue || !this.waAvatar) return; 70 | this.updateAvatar(); 71 | } 72 | 73 | updateAvatar() { 74 | const attrs = ["image", "label", "initials", "loading"]; 75 | attrs.forEach(attr => { 76 | const value = this.getAttribute(attr); 77 | if (value !== null) this.waAvatar.setAttribute(attr, value); 78 | else this.waAvatar.removeAttribute(attr); 79 | }); 80 | } 81 | 82 | disconnectedCallback() { 83 | this.waAvatar.removeEventListener("wa-error", this.#handleError); 84 | } 85 | 86 | #handleError = (event) => { 87 | this.dispatchEvent( 88 | new CustomEvent("virto-avatar-error", { 89 | bubbles: true, 90 | composed: true, 91 | detail: event.detail || { message: "Image didn’t load", url: this.getAttribute("image") } 92 | }) 93 | ); 94 | }; 95 | } 96 | 97 | if (!customElements.get(AvatarVirto.TAG)) { 98 | customElements.define(AvatarVirto.TAG, AvatarVirto); 99 | } -------------------------------------------------------------------------------- /sdk/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@virtonetwork/sdk", 3 | "version": "0.0.4-alpha.36", 4 | "main": "dist/cjs/index.js", 5 | "types": "dist/cjs/index.d.ts", 6 | "scripts": { 7 | "dev": "vite", 8 | "clean": "rm -rf dist", 9 | "build": "npm run clean && npm run build:web && npm run build:node && npm run build:cjs && npm run build:esm && npm run build:umd && npm run copy:papi", 10 | "copy:papi": "cp -r .papi dist/", 11 | "build:web": "vite build --config vite.config.web.mts && tsc --project tsconfig.web.json", 12 | "build:node": "vite build --config vite.config.node.mts && tsc --project tsconfig.node.json", 13 | "build:esm": "vite build --config vite.config.esm.mts && tsc --project tsconfig.esm.json", 14 | "build:cjs": "tsc --project tsconfig.cjs.json", 15 | "build:umd": "vite build --config vite.config.mts", 16 | "test": "npm run build && jest --config=jest.config.js", 17 | "test:watch": "npm run build && jest --config=jest.config.js --watch" 18 | }, 19 | "exports": { 20 | ".": { 21 | "require": { 22 | "types": "./dist/cjs/index.d.ts", 23 | "default": "./dist/cjs/index.js" 24 | }, 25 | "import": { 26 | "types": "./dist/esm/index.d.ts", 27 | "default": "./dist/esm/index.js" 28 | }, 29 | "default": "./dist/umd/index.umd.js" 30 | }, 31 | "./web": { 32 | "types": "./dist/web/index.d.ts", 33 | "default": "./dist/web/index.js" 34 | }, 35 | "./node": { 36 | "require": { 37 | "types": "./dist/node/index.d.ts", 38 | "default": "./dist/node/index.cjs" 39 | }, 40 | "import": { 41 | "types": "./dist/node/index.d.ts", 42 | "default": "./dist/node/index.mjs" 43 | } 44 | }, 45 | "./descriptors": { 46 | "types": "./.papi/descriptors/dist/index.d.ts", 47 | "module": "./.papi/descriptors/dist/index.mjs", 48 | "import": "./.papi/descriptors/dist/index.mjs", 49 | "require": "./.papi/descriptors/dist/index.js" 50 | } 51 | }, 52 | "devDependencies": { 53 | "@types/jest": "^29.5.14", 54 | "@types/jsonwebtoken": "^9.0.9", 55 | "@types/node": "^22.12.0", 56 | "@polkadot-api/descriptors": "file:../.papi/descriptors", 57 | "jest": "^29.7.0", 58 | "jest-environment-puppeteer": "^11.0.0", 59 | "jest-puppeteer": "^11.0.0", 60 | "puppeteer": "^24.1.1", 61 | "ts-jest": "^29.2.5", 62 | "typescript": "^5.7.3", 63 | "vite": "^6.0.11", 64 | "vite-plugin-wasm": "^3.4.1" 65 | }, 66 | "dependencies": { 67 | "@polkadot-api/substrate-bindings": "^0.16.3", 68 | "@polkadot-labs/hdkd-helpers": "^0.0.19", 69 | "@virtonetwork/authenticators-webauthn": "^1.1.3", 70 | "@virtonetwork/signer": "^1.1.0", 71 | "es-arraybuffer-base64": "^1.1.2", 72 | "jsonwebtoken": "^9.0.2", 73 | "nid-webauthn-emulator": "^0.2.4", 74 | "polkadot-api": "^1.17.2" 75 | }, 76 | "files": [ 77 | "dist", 78 | ".papi", 79 | "README.md", 80 | "LICENSE" 81 | ], 82 | "publishConfig": { 83 | "registry": "https://registry.npmjs.org/" 84 | } 85 | } -------------------------------------------------------------------------------- /lib/libwallet/README.md: -------------------------------------------------------------------------------- 1 | # Libwallet 2 | 3 | A lightweight and very portable library with simple to understand and use 4 | abstractions that allow creating chain-agnostic crypto wallets able to run in 5 | all kinds of environments like native applications, hosted wallets, the browser 6 | or even embedded hardware. 7 | 8 | Core principles: 9 | 10 | - **Ease of use** 11 | A high level public API abstracts blockchain and cryptography heavy concepts. 12 | i.e. `Account` abstracts handling of private keys and their metadata. 13 | `Vault` makes it simple to support multiple types of credentials and 14 | back-ends that store private keys in different ways, whether it is a 15 | cryptographic hardware module, a database or a file system. 16 | In the future this abstractions could be used to integrate with different 17 | ledger-like back-ends including regular bank accounts. 18 | 19 | - **Security** 20 | Written in safe Rust and based on of production ready crypto primitives. Also 21 | encourages good practices like the use of pin protected sub-accounts derived 22 | from the root account as a form of second factor authentication or a two step 23 | signing process where transactions are added to a queue for review before 24 | being signed. 25 | 26 | - **Multi-chain and extensibility** 27 | No assumptions are made about what the kind of private keys and signatures 28 | different vaults use to be able to support any chain's cryptography. 29 | Core functionality of wallets and accounts can be extended to support chain 30 | specific features. There is initial focus on supporting [Substrate][1] based 31 | chains adding features like formatting addresses using their network prefix 32 | or use metadata to validate what's being signed and simplify the creation of 33 | signed extrinsics. 34 | 35 | - **Portability and small footprint** 36 | Being `no_std` and WASM friendly users can create wallets for the wide range 37 | of platforms and computer architectures supported by Rust. Dependencies are 38 | kept to a minimum and any extra non-core functionality is set behind feature 39 | flags that users can enable only when needed. 40 | 41 | [1]: https://substrate.io/ 42 | 43 | ## Use in Virto 44 | 45 | libwallet is developed by the Virto Network team, a parachain focused in 46 | *decentralized commerce* infrastructure for the Polkadot ecosystem, Virto aims 47 | to bridge real world products/services and the common folk to the growing but 48 | not so user friendly world of interconnected blockchains. 49 | This library will be used as the core component of the **Wallet API**, one of 50 | Virto's *decentralizable*, composable and easy to use [HTTP APIs][2] that run 51 | as plugins of the [Valor][3] runtime. 52 | `SummaVault` will be our main vault implementation that integrates the Matrix 53 | protocol allowing users to sign-up to a homeserver using familiar credentials 54 | and have out of the box key management, multi-device support and encrypted 55 | account backup among others. 56 | 57 | [2]: https://github.com/virto-network/virto-apis 58 | [3]: https://github.com/virto-network/valor 59 | -------------------------------------------------------------------------------- /lib/sube/sube-js/.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | 4 | cache: cargo 5 | 6 | matrix: 7 | include: 8 | 9 | # Builds with wasm-pack. 10 | - rust: beta 11 | env: RUST_BACKTRACE=1 12 | addons: 13 | firefox: latest 14 | chrome: stable 15 | before_script: 16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 18 | - cargo install-update -a 19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 20 | script: 21 | - cargo generate --git . --name testing 22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 23 | # in any of our parent dirs is problematic. 24 | - mv Cargo.toml Cargo.toml.tmpl 25 | - cd testing 26 | - wasm-pack build 27 | - wasm-pack test --chrome --firefox --headless 28 | 29 | # Builds on nightly. 30 | - rust: nightly 31 | env: RUST_BACKTRACE=1 32 | before_script: 33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 35 | - cargo install-update -a 36 | - rustup target add wasm32-unknown-unknown 37 | script: 38 | - cargo generate --git . --name testing 39 | - mv Cargo.toml Cargo.toml.tmpl 40 | - cd testing 41 | - cargo check 42 | - cargo check --target wasm32-unknown-unknown 43 | - cargo check --no-default-features 44 | - cargo check --target wasm32-unknown-unknown --no-default-features 45 | - cargo check --no-default-features --features console_error_panic_hook 46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 49 | 50 | # Builds on beta. 51 | - rust: beta 52 | env: RUST_BACKTRACE=1 53 | before_script: 54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 56 | - cargo install-update -a 57 | - rustup target add wasm32-unknown-unknown 58 | script: 59 | - cargo generate --git . --name testing 60 | - mv Cargo.toml Cargo.toml.tmpl 61 | - cd testing 62 | - cargo check 63 | - cargo check --target wasm32-unknown-unknown 64 | - cargo check --no-default-features 65 | - cargo check --target wasm32-unknown-unknown --no-default-features 66 | - cargo check --no-default-features --features console_error_panic_hook 67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 68 | # Note: no enabling the `wee_alloc` feature here because it requires 69 | # nightly for now. 70 | -------------------------------------------------------------------------------- /lib/pjs-rs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PJS extension from Rust 8 | 75 | 76 | 77 | 78 |
79 | 80 | 82 | 83 | 84 |
85 | ~ 86 | 87 | 88 | 120 | 121 | -------------------------------------------------------------------------------- /sdk/js/src/storage/SignerSerializer.ts: -------------------------------------------------------------------------------- 1 | import { PolkadotSigner } from "polkadot-api"; 2 | import { SignFn } from "../types"; 3 | import { getPolkadotSigner } from "polkadot-api/signer"; 4 | import { sr25519CreateDerive } from "@polkadot-labs/hdkd"; 5 | import { Blake2256 } from '@polkadot-api/substrate-bindings'; 6 | import { mergeUint8 } from "polkadot-api/utils"; 7 | 8 | export interface SerializableSignerData { 9 | publicKey: string; 10 | miniSecret: string; 11 | derivationPath: string; 12 | hashedUserId?: string; 13 | address?: string; 14 | } 15 | 16 | /** 17 | * Utility to serialize and deserialize signers 18 | * 19 | * Allows converting a PolkadotSigner 20 | * into JSON data that can be stored in any storage, and then recreate it. 21 | */ 22 | export class SignerSerializer { 23 | /** 24 | * Converts a signer into serializable data 25 | */ 26 | static serialize( 27 | recreationData: { 28 | miniSecret: Uint8Array; 29 | derivationPath: string; 30 | originalPublicKey: Uint8Array; 31 | hashedUserId?: Uint8Array; 32 | address?: string; 33 | } 34 | ): SerializableSignerData { 35 | return { 36 | publicKey: Buffer.from(recreationData.originalPublicKey).toString('hex'), 37 | miniSecret: Buffer.from(recreationData.miniSecret).toString('hex'), 38 | derivationPath: recreationData.derivationPath, 39 | hashedUserId: recreationData.hashedUserId ? Buffer.from(recreationData.hashedUserId).toString('hex') : undefined, 40 | address: recreationData.address 41 | }; 42 | } 43 | 44 | /** 45 | * Recreates a signer from serializable data 46 | */ 47 | static deserialize(data: SerializableSignerData): PolkadotSigner & { sign: SignFn } { 48 | const miniSecret = new Uint8Array(Buffer.from(data.miniSecret, 'hex')); 49 | const originalPublicKey = new Uint8Array(Buffer.from(data.publicKey, 'hex')); 50 | 51 | // Recreate the keypair 52 | const derive = sr25519CreateDerive(miniSecret); 53 | const keypair = derive(data.derivationPath); 54 | 55 | // Recreate the signer 56 | const signer = getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); 57 | 58 | // Add the sign function 59 | Object.defineProperty(signer, "sign", { 60 | value: keypair.sign, 61 | configurable: false, 62 | }); 63 | 64 | // Restore the modified publicKey if it exists 65 | if (data.hashedUserId) { 66 | const hashedUserId = new Uint8Array(Buffer.from(data.hashedUserId, 'hex')); 67 | signer.publicKey = Blake2256( 68 | mergeUint8(new Uint8Array(32).fill(0), hashedUserId) 69 | ); 70 | } else { 71 | signer.publicKey = originalPublicKey; 72 | } 73 | 74 | // Restore the server address if it exists 75 | if (data.address) { 76 | Object.defineProperty(signer, "_serverAddress", { 77 | value: data.address, 78 | writable: false, 79 | enumerable: false, 80 | configurable: false, 81 | }); 82 | } 83 | 84 | return signer as PolkadotSigner & { sign: SignFn }; 85 | } 86 | 87 | /** 88 | * Verifies if the data is from a serialized signer 89 | */ 90 | static isSerializableSignerData(data: any): data is SerializableSignerData { 91 | return data && 92 | typeof data === 'object' && 93 | typeof data.publicKey === 'string' && 94 | typeof data.miniSecret === 'string' && 95 | typeof data.derivationPath === 'string'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sdk/js/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Virto Network SDK Demo 8 | 42 | 43 | 44 |
45 |

Virto Network SDK Demo

46 |
47 |

Register User

48 | 49 | 50 | 51 |
52 |
53 |

Connect User

54 | 55 |
56 |
57 |

Sign Extrinsic

58 | 63 | 64 |
65 |
66 |

Transfer Balance

67 | 68 | 69 | 70 | 71 |
72 |
73 |

Batch Demo (Transfer + Remark)

74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 |

Transaction History

82 |
83 |

No transactions yet...

84 |
85 | 86 |
87 | 88 |
89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/sube/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sube" 3 | description = "SUBmit Extrinsics to a Substrate node" 4 | license = "Apache-2.0" 5 | version = "1.0.0" 6 | authors = ["Daniel Olano "] 7 | edition = "2021" 8 | repository = "https://github.com/valibre-org/virto-dk/sube" 9 | 10 | [dependencies] 11 | async-once-cell = "0.4.4" 12 | blake2 = { version = "0.10.5", default-features = false } 13 | codec = { version = "3.1.2", package = "parity-scale-codec", default-features = false } 14 | frame-metadata = { version = "16.0.0", default-features = false, features = [ 15 | "serde_full", 16 | "decode", 17 | ] } 18 | hex = { version = "0.4.3", default-features = false, features = ["alloc"] } 19 | jsonrpc = { version = "0.12.1", default-features = false, optional = true } 20 | log = "0.4.17" 21 | scale-info = { version = "2.1.1", default-features = false, optional = true } 22 | scales = { path = "../scales", package = "scale-serialization", default-features = false, features = [ 23 | "codec", 24 | "experimental-serializer", 25 | "json", 26 | "std", 27 | ] } 28 | serde = { version = "1.0.137", default-features = false } 29 | # TODO: shouldn't be a base dependeny. remove after: https://github.com/virto-network/virto-sdk/issues/53 30 | serde_json = { version = "1.0.80", default-features = false, features = [ 31 | "alloc", 32 | "arbitrary_precision", 33 | ] } 34 | twox-hash = { version = "1.6.2", default-features = false } 35 | url = "2.5.0" 36 | 37 | # http backend 38 | reqwest = { version = "0.12.5", optional = true, features = ["json"]} 39 | 40 | # ws backend 41 | futures-channel = { version = "0.3.21", default-features = false, features = [ 42 | "alloc", 43 | ], optional = true } 44 | futures-util = { version = "0.3.21", default-features = false, features = [ 45 | "sink", 46 | ], optional = true } 47 | 48 | async-tls = { version = "0.11.0", default-features = false, optional = true } 49 | 50 | # bin target 51 | async-std = { version = "1.11.0", optional = true } 52 | paste = { version = "1.0" } 53 | wasm-bindgen = { version = "0.2.92", optional = true } 54 | once_cell = { version = "1.17.1", optional = true } 55 | heapless = "0.8.0" 56 | anyhow = { version = "1.0.40", optional = true } 57 | rand_core = { version = "0.6.3", optional = true } 58 | ewebsock = { git = "https://github.com/S0c5/ewebsock.git", optional = true, branch = "enhacement/aviod-blocking-operations-with-mpsc-futures" } 59 | env_logger = "0.11.3" 60 | no-std-async = "1.1.2" 61 | 62 | 63 | [dev-dependencies] 64 | async-std = { version = "1.11.0", features = ["attributes", "tokio1"] } 65 | hex-literal = "0.3.4" 66 | libwallet = { path = "../libwallet", default-features = false, features = [ 67 | "substrate", 68 | "mnemonic", 69 | "sr25519", 70 | "util_pin", 71 | "rand", 72 | "std", 73 | ] } 74 | rand_core = "0.6.3" 75 | 76 | [features] 77 | default = ["v14"] 78 | test = ["std", "wss", "http", "json", "v14", "dep:async-std", "dep:rand_core"] 79 | http = ["dep:jsonrpc", "dep:reqwest"] 80 | http-web = ["dep:jsonrpc", "dep:wasm-bindgen", "dep:reqwest"] 81 | json = ["scales/json"] 82 | std = [] 83 | no_std = [] 84 | 85 | 86 | v14 = ["dep:scale-info", "frame-metadata/current"] 87 | ws = [ 88 | "dep:async-std", 89 | "dep:ewebsock", 90 | "dep:futures-channel", 91 | "dep:futures-util", 92 | "dep:jsonrpc", 93 | "async-std/unstable", 94 | ] 95 | wss = ["dep:async-tls", "ws", "ewebsock/tls", "async-std/unstable"] 96 | examples = ["dep:rand_core"] 97 | js = ["http-web", "json", "v14", 'async-std/unstable', "wss", "dep:rand_core"] 98 | 99 | [package.metadata.docs.rs] 100 | features = ["http"] 101 | 102 | [workspace] 103 | members = [ 104 | "sube-js", 105 | "cli" 106 | ] 107 | -------------------------------------------------------------------------------- /sdk/js/examples/server/README.md: -------------------------------------------------------------------------------- 1 | # Virto SDK - Server Example 2 | 3 | This example shows how to implement a server that uses Virto SDK for authentication and transaction signing, using JWT to ensure the security of user sessions. 4 | 5 | ## Features 6 | 7 | - Express API that acts as an intermediary between the client and the Federate Network 8 | - User and session management 9 | - User registration verification 10 | - WebAuthn authentication completed on the server 11 | - JWT authentication to protect signing operations 12 | - Transaction signing by the authenticated user 13 | 14 | ## Requirements 15 | 16 | - Node.js (v22 or higher) 17 | - npm or yarn 18 | 19 | ## Installation 20 | 21 | 1. Clone the repository 22 | 2. Install dependencies: 23 | 24 | ```bash 25 | cd sdk/js/examples/server 26 | npm install 27 | ``` 28 | 29 | ## Configuration 30 | 31 | The server uses the following environment variables: 32 | 33 | - `PORT`: Port where the server will run (default: 9000) 34 | - `JWT_SECRET`: Secret key for signing JWT tokens (default: an example key is used) 35 | 36 | For better security in production, set the `JWT_SECRET` variable with a strong and unique value. 37 | 38 | ## Execution 39 | 40 | ```bash 41 | npm run dev 42 | ``` 43 | 44 | The server will start at `http://localhost:14000` or on the port specified in the environment variables. 45 | 46 | ## Endpoints 47 | 48 | ### User Registration and Connection 49 | 50 | - `GET /check-registered/:userId`: Verifies if a user is registered 51 | - `POST /custom-register`: Completes the registration process initiated on the client 52 | - `POST /custom-connect`: Completes the connection process and returns a JWT token for authorization 53 | 54 | ### Transaction Signing 55 | 56 | - `POST /sign`: Signs a transaction using JWT authentication 57 | - Requires `Authorization: Bearer ` header 58 | - Token verification and userId extraction is performed by the SDK 59 | - The token has a default validity of 10 minutes 60 | 61 | ## JWT Functionality 62 | 63 | 1. When a user connects, the serverSDK generates a JWT token that contains: 64 | - The user ID (userId) 65 | - The wallet address 66 | - Issue date (iat) 67 | - Expiration date (exp) 68 | 69 | 2. This token is returned to the client, which must store it securely. 70 | 71 | 3. For signing operations, the client must include the token in the `Authorization` header. 72 | 73 | 4. The server extracts the token and passes it to the SDK, which: 74 | - Verifies that the token signature is valid 75 | - Checks that the token has not expired 76 | - Confirms that the wallet address in the token matches the current session 77 | - Extracts the userId and uses it to sign the command 78 | 79 | 5. If the token is valid, the SDK proceeds with signing; otherwise, it returns a specific error. 80 | 81 | ## Delegation of Responsibilities 82 | 83 | - **Server**: Only responsible for extracting the token from the HTTP request and passing it to the SDK 84 | - **SDK**: Handles all token verification logic, ensuring consistency and security 85 | 86 | ## Specific Error Responses for JWT 87 | 88 | The SDK returns specific error codes that the server transmits to the client: 89 | 90 | - `E_JWT_EXPIRED`: The token has expired (401 Unauthorized) 91 | - `E_JWT_INVALID`: The token is invalid or has been tampered with (401 Unauthorized) 92 | - `E_JWT_UNKNOWN`: Unknown error in token verification (401 Unauthorized) 93 | - `E_SESSION_NOT_FOUND`: The session associated with the token does not exist (404 Not Found) 94 | - `E_ADDRESS_MISMATCH`: The token contains an address that does not match the session (401 Unauthorized) 95 | 96 | ## Client Integration Example 97 | 98 | See the [client example](../client/README.md) to understand how a web client integrates with this server. 99 | -------------------------------------------------------------------------------- /lib/libwallet/src/vault/simple.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{seed_from_entropy, Pin}; 2 | use crate::{ 3 | vault::utils::{AccountSigner, RootAccount}, 4 | Vault, 5 | }; 6 | use core::marker::PhantomData; 7 | 8 | /// A vault that holds secrets in memory 9 | pub struct Simple { 10 | locked: Option<[u8; N]>, 11 | unlocked: Option<[u8; N]>, 12 | _phantom: PhantomData, 13 | } 14 | 15 | impl Simple { 16 | /// A vault with a random seed, once dropped the the vault can't be restored 17 | /// 18 | /// ``` 19 | /// # use libwallet::{vault, Error, Derive, Pair, Vault}; 20 | /// # type SimpleVault = vault::Simple; 21 | /// # type Result = std::result::Result<(), ::Error>; 22 | /// # #[async_std::main] async fn main() -> Result { 23 | /// let mut vault = SimpleVault::generate(&mut rand_core::OsRng); 24 | /// let root = vault.unlock(None, None).await?; 25 | /// # Ok(()) 26 | /// } 27 | /// ``` 28 | #[cfg(feature = "rand")] 29 | pub fn generate(rng: &mut R) -> Self 30 | where 31 | R: rand_core::CryptoRng + rand_core::RngCore, 32 | { 33 | Simple { 34 | locked: Some(crate::util::random_bytes::<_, N>(rng)), 35 | unlocked: None, 36 | _phantom: Default::default(), 37 | } 38 | } 39 | 40 | #[cfg(all(feature = "rand", feature = "mnemonic"))] 41 | pub fn generate_with_phrase(rng: &mut R) -> (Self, mnemonic::Mnemonic) 42 | where 43 | R: rand_core::CryptoRng + rand_core::RngCore, 44 | { 45 | let phrase = crate::util::gen_phrase(rng, Default::default()); 46 | (Self::from_phrase(&phrase), phrase) 47 | } 48 | 49 | 50 | #[cfg(feature = "mnemonic")] 51 | // Provide your own seed 52 | pub fn from_phrase(phrase: impl AsRef) -> Self { 53 | use core::convert::TryInto; 54 | mnemonic::Mnemonic::validate(phrase.as_ref()).expect("its a valid mnemonic"); 55 | // Count the number of words in the phrase 56 | let mnemonic = mnemonic::Mnemonic::from_phrase(phrase.as_ref()).expect("its a valid mnemonic"); 57 | 58 | let raw_entropy = mnemonic.entropy(); 59 | 60 | Simple { 61 | locked: Some(raw_entropy.try_into().expect("its a valid entropy")), 62 | unlocked: None, 63 | _phantom: Default::default(), 64 | } 65 | } 66 | 67 | fn get_key(&self, pin: Pin) -> Result { 68 | if let Some(entropy) = self.unlocked { 69 | let seed = &entropy; 70 | seed_from_entropy!(seed, pin); 71 | Ok(RootAccount::from_bytes(seed)) 72 | } else { 73 | Err(Error) 74 | } 75 | } 76 | } 77 | 78 | 79 | 80 | #[derive(Debug)] 81 | pub struct Error; 82 | impl core::fmt::Display for Error { 83 | fn fmt(&self, _f: &mut core::fmt::Formatter) -> core::fmt::Result { 84 | Ok(()) 85 | } 86 | } 87 | #[cfg(feature = "std")] 88 | impl std::error::Error for Error {} 89 | 90 | impl, const N: usize> Vault for Simple { 91 | type Credentials = Option; 92 | type Error = Error; 93 | type Id = Option; 94 | type Account = AccountSigner; 95 | 96 | async fn unlock( 97 | &mut self, 98 | path: Self::Id, 99 | creds: impl Into, 100 | ) -> Result { 101 | self.unlocked = self.locked.take(); 102 | let pin = creds.into(); 103 | let root_account = self.get_key(pin.unwrap_or_default())?; 104 | let path = path.as_ref().map(|x| x.as_ref()); 105 | Ok(AccountSigner::new(path).unlock(&root_account)) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/sube/sube-js/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod util; 2 | 3 | use core::convert::TryInto; 4 | use serde::{Deserialize, Serialize}; 5 | use serde_wasm_bindgen; 6 | use sube::{ 7 | sube, Error as SubeError, ExtrinsicBody, JsonValue, Response, SubeBuilder 8 | }; 9 | use util::*; 10 | use wasm_bindgen::prelude::*; 11 | use wasm_bindgen::JsValue; 12 | extern crate console_error_panic_hook; 13 | use wasm_logger; 14 | 15 | #[cfg(feature = "wee_alloc")] 16 | #[global_allocator] 17 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 18 | 19 | #[wasm_bindgen] 20 | extern "C" { 21 | // Use `js_namespace` here to bind `console.log(..)` instead of just 22 | // `log(..)` 23 | #[wasm_bindgen(js_namespace = console)] 24 | fn log(s: &str); 25 | } 26 | 27 | 28 | 29 | #[derive(Serialize, Deserialize, Debug)] 30 | struct ExtrinsicBodyWithFrom { 31 | from: Vec, 32 | call: ExtrinsicBody, 33 | } 34 | 35 | #[wasm_bindgen] 36 | pub async fn sube_js( 37 | url: &str, 38 | params: JsValue, 39 | signer: Option, 40 | ) -> Result { 41 | 42 | wasm_logger::init(wasm_logger::Config::default()); 43 | console_error_panic_hook::set_once(); 44 | 45 | log::info!("sube_js: {:?}", params); 46 | 47 | if params.is_undefined() { 48 | let response = sube!(url) 49 | .await 50 | .map_err(|e| JsError::new(&format!("Error querying: {:?}", &e.to_string())))?; 51 | 52 | let value = match response { 53 | v @ Response::Value(_) | v @ Response::Meta(_) | v @ Response::Registry(_) => { 54 | let value = serde_wasm_bindgen::to_value(&v) 55 | .map_err(|_| JsError::new("failed to serialize response"))?; 56 | Ok(value) 57 | } 58 | _ => Err(JsError::new("Nonve value at query")), 59 | }?; 60 | 61 | return Ok(value); 62 | } 63 | 64 | let mut extrinsic_value: ExtrinsicBodyWithFrom = serde_wasm_bindgen::from_value(params)?; 65 | 66 | extrinsic_value.call.body = decode_addresses(&extrinsic_value.call.body); 67 | 68 | log::info!("new extrinsic_value: {:?}", extrinsic_value); 69 | 70 | let signer = sube::SignerFn::from(( 71 | extrinsic_value.from, 72 | |message: &[u8]| { 73 | let message = message.to_vec(); 74 | let signer = signer 75 | .clone(); 76 | 77 | async move { 78 | let promise = signer 79 | .ok_or(SubeError::BadInput)? 80 | .call1( 81 | &JsValue::null(), 82 | &JsValue::from(js_sys::Uint8Array::from(message.to_vec().as_ref())), 83 | ) 84 | .map_err(|_| SubeError::Signing)?; 85 | 86 | let response = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise)) 87 | .await 88 | .map_err(|_| SubeError::Signing)?; 89 | 90 | let vec: Vec = serde_wasm_bindgen::from_value(response) 91 | .map_err(|_| SubeError::Encode("Unknown value to decode".into()))?; 92 | 93 | let buffer: [u8; 64] = vec.try_into().expect("slice with incorrect length"); 94 | 95 | Ok(buffer) 96 | } 97 | }, 98 | )); 99 | 100 | let value = SubeBuilder::default() 101 | .with_url(url) 102 | .with_body(extrinsic_value.call.body) 103 | .with_signer(signer) 104 | .await 105 | .map_err(|e| JsError::new(&format!("Error trying: {:?}", e.to_string())))?; 106 | 107 | 108 | match value { 109 | Response::Void => Ok(JsValue::null()), 110 | _ => Err(JsError::new("Unknown Response")), 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /components/button.js: -------------------------------------------------------------------------------- 1 | import { html, css } from "./utils.js"; 2 | import { globalStyles } from "./globalStyles.js"; 3 | 4 | const buttonTp = html` 5 | 6 | `; 7 | 8 | const buttonCss = await css` 9 | :host { 10 | display: inline-block; 11 | width: 100%; 12 | } 13 | wa-button::part(base) { 14 | font-family: Outfit, sans-serif; 15 | width: 100%; 16 | height: 44px; 17 | min-height: 44px; 18 | padding: 12px; 19 | border-radius: 1000px; 20 | border: 1px solid #1A1A1A1F; 21 | opacity: 1; 22 | background-color: var(--green); 23 | color: var(--whitesmoke); 24 | transition: background-color 500ms ease, color 500ms ease; 25 | white-space: nowrap; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | } 29 | wa-button::part(base):hover { 30 | background-color: var(--lightgreen); 31 | color: var(--darkslategray); 32 | } 33 | wa-button::part(base):focus { 34 | outline: 2px solid var(--darkslategray); 35 | } 36 | :host([variant="secondary"]) > wa-button::part(base) { 37 | background-color: var(--extra-light-green); 38 | color: var(--darkslategray); 39 | border: 1px solid var(--lightgreen); 40 | } 41 | :host([variant="secondary"]) wa-button::part(base):hover, 42 | :host([variant="secondary"]) > wa-button::part(base):focus { 43 | background-color: var(--whitish-green); 44 | } 45 | :host([variant="secondary"]) > wa-button::part(base):focus { 46 | outline: 2px solid var(--green); 47 | } 48 | :host([disabled]) > wa-button::part(base), :host([disabled]) > wa-button::part(base):hover { 49 | background-color: var(--grey-green); 50 | color: var(--darkslategray); 51 | border: transparent; 52 | } 53 | { 54 | `; 55 | 56 | export class ButtonVirto extends HTMLElement { 57 | static TAG = "virto-button"; 58 | static formAssociated = true; 59 | #internals; 60 | 61 | constructor() { 62 | super(); 63 | this.attachShadow({ mode: "open" }); 64 | this.shadowRoot.appendChild(buttonTp.content.cloneNode(true)); 65 | this.shadowRoot.adoptedStyleSheets = [globalStyles, buttonCss]; 66 | 67 | this.waButton = this.shadowRoot.querySelector("wa-button"); 68 | this.waButton.textContent = this.getAttribute("label") || "Button"; 69 | this.#internals = this.attachInternals(); 70 | this.#syncAttributes(); 71 | } 72 | 73 | connectedCallback() { 74 | this.waButton.addEventListener("click", this.#handleClick); 75 | } 76 | 77 | static observedAttributes = ["label", "variant", "disabled", "type", "loading"]; 78 | 79 | attributeChangedCallback(name, oldValue, newValue) { 80 | if (!this.waButton) return; 81 | if (name === "label") { 82 | this.waButton.textContent = newValue || "Button"; 83 | } else { 84 | this.#syncAttributes(); 85 | } 86 | } 87 | 88 | #syncAttributes() { 89 | const attrs = ["variant", "disabled", "type", "loading"]; 90 | attrs.forEach(attr => { 91 | const value = this.getAttribute(attr); 92 | if (value !== null) this.waButton.setAttribute(attr, value); 93 | else this.waButton.removeAttribute(attr); 94 | }); 95 | if (this.getAttribute("type") === "submit") { 96 | this.#internals.setFormValue(this.getAttribute("value") || "submit"); 97 | } 98 | } 99 | 100 | #handleClick = () => { 101 | if (this.getAttribute("type") === "submit" && this.#internals.form && !this.hasAttribute("disabled")) { 102 | this.#internals.form.requestSubmit(); 103 | } 104 | }; 105 | 106 | get form() { return this.#internals.form; } 107 | get name() { return this.getAttribute("name"); } 108 | get type() { return this.getAttribute("type") || "button"; } 109 | get value() { return this.getAttribute("value") || "submit"; } 110 | 111 | } 112 | 113 | if (!customElements.get(ButtonVirto.TAG)) { 114 | customElements.define(ButtonVirto.TAG, ButtonVirto); 115 | } -------------------------------------------------------------------------------- /components/components.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom Dropdown Example 7 | 8 | 20 | 21 | 22 | 23 |

Avatar

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

Buttons

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 |

Input

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 77 | 78 | -------------------------------------------------------------------------------- /sdk/js/examples/client/README.md: -------------------------------------------------------------------------------- 1 | # Virto SDK - Client Example 2 | 3 | This example shows how to implement a web client that uses Virto SDK to authenticate and authorize the user, and how to handle JWT-based authentication for securely signing transactions. 4 | 5 | ## Features 6 | 7 | - User interface for registration and connection process 8 | - WebAuthn implementation for secure authentication 9 | - JWT authentication support for signing requests 10 | - JWT token storage in localStorage for session persistence 11 | - Automatic token expiration management 12 | - Complete connection and signing flow 13 | 14 | ## Requirements 15 | 16 | - Node.js (v14 or higher) 17 | - npm or yarn 18 | - A server compatible with Virto SDK (see [server example](../server/README.md)) 19 | - Modern browser with WebAuthn support 20 | 21 | ## Installation 22 | 23 | 1. Clone the repository 24 | 2. Install dependencies: 25 | 26 | ```bash 27 | cd sdk/js/examples/client 28 | npm install 29 | ``` 30 | 31 | ## Execution 32 | 33 | ```bash 34 | npm run dev 35 | ``` 36 | 37 | The application will start in development mode and will be available at `http://localhost:5173` or similar. 38 | 39 | ## Using the application 40 | 41 | 1. **Enter a user ID** - Can be any unique identifier 42 | 2. **Prepare and complete registration** - If it's the first time using this ID 43 | 3. **Prepare and complete connection** - For already registered users 44 | 4. **Sign commands** - Once connected, you can sign commands with your wallet 45 | 46 | ## Session Persistence 47 | 48 | This client implements session persistence using localStorage: 49 | 50 | 1. The JWT token and user ID are automatically saved in localStorage when: 51 | - A successful connection is completed 52 | - A valid JWT token is received from the server 53 | 54 | 2. The session is automatically restored when loading the page: 55 | - If there is session data in localStorage, it is loaded when starting the application 56 | - The user can sign commands immediately without having to reconnect 57 | 58 | ## JWT Authentication Process 59 | 60 | ### Connection 61 | 62 | 1. When completing the connection (`completeConnection()`), the client receives a JWT token from the server. 63 | 2. This token is stored in memory (`authToken`) and in localStorage for later use. 64 | 3. The token contains identity information such as userId and wallet address. 65 | 66 | ### Signing commands 67 | 68 | 1. When signing commands (`signCommand()`), the client: 69 | - Checks if it has a valid JWT token. 70 | - If it exists, adds the token in the `Authorization: Bearer ` header. 71 | - Sends the request to the secure `/sign` endpoint. 72 | 73 | 2. If the token has expired: 74 | - The server returns a 401 error. 75 | 76 | ## Backend Integration 77 | 78 | This client is designed to work with the [example server](../server/README.md) included in this repository, but it can be adapted to work with any backend compatible with the Virto SDK API. 79 | 80 | ## Execution 81 | 82 | ```bash 83 | # Start in development mode 84 | npm run dev 85 | ``` 86 | 87 | Once started, you can access the application at [http://localhost:5173](http://localhost:5173). 88 | 89 | ## Features 90 | 91 | ### Complete User Registration 92 | 93 | This flow uses the SDK's `register()` method, which handles the entire registration process in a single operation within the client. 94 | 95 | ### Split Client/Server Registration 96 | 97 | This flow demonstrates the split architecture: 98 | 99 | 1. **Step 1: Prepare Registration on Client** 100 | - Uses the SDK's `prepareRegistration()` method 101 | - Creates WebAuthn credentials locally in the browser 102 | - Generates the necessary data to complete the registration 103 | 104 | 2. **Step 2: Complete Registration on Server** 105 | - Sends the prepared data to the custom server 106 | - The server will complete the registration process 107 | 108 | ## How to Test 109 | 110 | 1. First start the example server located in `../server` 111 | 2. Start this client with `npm run dev` 112 | 3. Open the browser and use the interface to test both registration flows 113 | -------------------------------------------------------------------------------- /lib/libwallet/src/vault/os.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use crate::{ 4 | mnemonic::{Language, Mnemonic}, 5 | util::{seed_from_entropy, Pin}, 6 | vault::utils::{AccountSigner, RootAccount}, 7 | Vault, 8 | }; 9 | use arrayvec::ArrayVec; 10 | use keyring; 11 | 12 | const SERVICE: &str = "libwallet_account"; 13 | 14 | /// A vault that stores keys in the default OS secure store 15 | pub struct OSKeyring { 16 | entry: keyring::Entry, 17 | root: Option, 18 | auto_generate: Option, 19 | _phantom: PhantomData, 20 | } 21 | 22 | impl OSKeyring { 23 | /// Create a new OSKeyring vault for the given user. 24 | /// The optional `lang` instructs the vault to generarte a backup phrase 25 | /// in the given language in case one does not exist. 26 | pub fn new(uname: &str, lang: impl Into>) -> Self { 27 | OSKeyring { 28 | entry: keyring::Entry::new(SERVICE, &uname), 29 | root: None, 30 | auto_generate: lang.into(), 31 | _phantom: PhantomData::default(), 32 | } 33 | } 34 | 35 | /// Relace the stored backap phrase with a new one. 36 | pub fn update(&self, phrase: &str) -> Result<(), ()> { 37 | self.entry.set_password(phrase).map_err(|_| ()) 38 | } 39 | 40 | /// Returned the stored phrase from the OS secure storage 41 | pub fn get(&self) -> Result { 42 | self.entry 43 | .get_password() 44 | // .inspect_err(|e| { 45 | // dbg!(e); 46 | // }) 47 | .map_err(|_| Error::Keyring) 48 | } 49 | 50 | fn get_key(&self, pin: Pin) -> Result { 51 | let phrase = self 52 | .get()? 53 | .parse::() 54 | .map_err(|_| Error::BadPhrase)?; 55 | 56 | let seed = phrase.entropy(); 57 | seed_from_entropy!(seed, pin); 58 | Ok(RootAccount::from_bytes(seed)) 59 | } 60 | 61 | // Create new random seed and save it in the OS keyring. 62 | fn generate(&self, pin: Pin, lang: Language) -> Result { 63 | let phrase = crate::util::gen_phrase(&mut rand_core::OsRng, lang); 64 | 65 | let seed = phrase.entropy(); 66 | seed_from_entropy!(seed, pin); 67 | let root = RootAccount::from_bytes(seed); 68 | 69 | self.entry 70 | .set_password(phrase.phrase()) 71 | // .inspect_err(|e| { 72 | // dbg!(e); 73 | // }) 74 | .map_err(|e| { 75 | dbg!(e); 76 | Error::Keyring 77 | })?; 78 | 79 | Ok(root) 80 | } 81 | } 82 | 83 | #[derive(Debug)] 84 | pub enum Error { 85 | Keyring, 86 | NotFound, 87 | BadPhrase, 88 | } 89 | 90 | impl core::fmt::Display for Error { 91 | fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { 92 | match self { 93 | Error::Keyring => write!(f, "OS Key storage error"), 94 | Error::NotFound => write!(f, "Key not found"), 95 | Error::BadPhrase => write!(f, "Mnemonic is invalid"), 96 | } 97 | } 98 | } 99 | 100 | #[cfg(feature = "std")] 101 | impl std::error::Error for Error {} 102 | 103 | impl> Vault for OSKeyring { 104 | type Credentials = Pin; 105 | type Error = Error; 106 | type Id = Option; 107 | type Account = AccountSigner; 108 | 109 | async fn unlock( 110 | &mut self, 111 | account: Self::Id, 112 | cred: impl Into, 113 | ) -> Result { 114 | let pin = cred.into(); 115 | self.get_key(pin) 116 | .or_else(|err| { 117 | self.auto_generate 118 | .ok_or(err) 119 | .and_then(|l| self.generate(pin, l)) 120 | }) 121 | .map(|r| { 122 | let acc = AccountSigner::new(account.as_ref().map(|x| x.as_ref())).unlock(&r); 123 | self.root = Some(r); 124 | acc 125 | }) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /components/virto-notification/notification.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '@esm-bundle/chai'; 2 | import './notification.js'; 3 | 4 | describe('Rendering', () => { 5 | let element; 6 | 7 | beforeEach(() => { 8 | // TODO: Create it just once if possible and not problematic 9 | document.body.innerHTML = ''; 10 | element = document.createElement('virto-notification'); 11 | document.body.appendChild(element); 12 | }); 13 | 14 | it('should render the notification component', () => { 15 | expect(element).to.exist; 16 | expect(element.tagName.toLowerCase()).to.equal('virto-notification'); 17 | }); 18 | 19 | it('should create a Shadow DOM root', () => { 20 | expect(element.shadowRoot).to.exist; 21 | }); 22 | 23 | it('Shadow DOM should contain the element', () => { 24 | const callout = element.shadowRoot.querySelector('wa-callout'); 25 | expect(callout).to.exist; 26 | expect(callout.tagName.toLowerCase()).to.equal('wa-callout'); 27 | }); 28 | 29 | it('should test if the virto-notification component was defined', () => { 30 | const isDefined = window.customElements.get('virto-notification') !== undefined; 31 | expect(isDefined).to.be.true; 32 | }); 33 | 34 | it('the component tag should start with virto-', () => { 35 | expect(element.tagName.toLowerCase().startsWith('virto-')).to.be.true; 36 | }); 37 | 38 | it('should be able to take content using innerHTML', () => { 39 | element.innerHTML = 'Warning!'; 40 | expect(element.innerHTML).to.contain('Warning!'); 41 | }); 42 | }); 43 | 44 | describe('Attributes', () => { 45 | let element; 46 | 47 | beforeEach(() => { 48 | document.body.innerHTML = ''; 49 | element = document.createElement('virto-notification'); 50 | document.body.appendChild(element); 51 | }); 52 | 53 | it('should take an attribute called variant', () => { 54 | element.setAttribute('variant', 'success'); 55 | expect(element.getAttribute('variant')).to.equal('success'); 56 | }); 57 | 58 | it('should take an attribute called appearance', () => { 59 | element.setAttribute('appearance', 'accent'); 60 | expect(element.getAttribute('appearance')).to.equal('accent'); 61 | }); 62 | 63 | it('should take an attribute called size', () => { 64 | element.setAttribute('size', 'small'); 65 | expect(element.getAttribute('size')).to.equal('small'); 66 | }); 67 | }); 68 | 69 | describe('Styles', () => { 70 | let element; 71 | 72 | beforeEach(() => { 73 | document.body.innerHTML = ''; 74 | element = document.createElement('virto-notification'); 75 | document.body.appendChild(element); 76 | }); 77 | 78 | it('should change its style according to the value on variant attribute', () => { 79 | element.setAttribute('variant', 'success'); 80 | const callout = element.shadowRoot.querySelector('wa-callout'); 81 | const bgColor = window.getComputedStyle(callout).backgroundColor; 82 | expect(bgColor).to.be.oneOf(['rgb(144, 238, 144)', 'rgb(221, 251, 224)', 'rgb(218, 251, 221)']); 83 | }); 84 | 85 | it('should add styles using adoptedStyleSheets', () => { 86 | expect(element.shadowRoot.adoptedStyleSheets.length).to.be.greaterThan(0); 87 | }); 88 | 89 | }); 90 | 91 | describe('Events', () => { 92 | let element; 93 | 94 | beforeEach(() => { 95 | document.body.innerHTML = ''; 96 | element = document.createElement('virto-notification'); 97 | document.body.appendChild(element); 98 | }); 99 | 100 | it('responds to virto-info event'); 101 | it('should accept the event virto-info'); 102 | it('should accept the event virto-success'); 103 | it('should accept the event virto-warning'); 104 | it('should accept the event virto-danger'); 105 | }); 106 | 107 | describe('User Interactions', () => { 108 | it('should be triggered with the event virto-info by an action the user performed'); 109 | it('should be triggered with the event virto-success by an action the user'); 110 | it('should be triggered with the event virto-error by an action the user performed'); 111 | it('should be triggered with the event virto-warning by an action the user performed'); 112 | }); 113 | -------------------------------------------------------------------------------- /lib/libwallet/src/util.rs: -------------------------------------------------------------------------------- 1 | use core::{iter, ops}; 2 | #[cfg(feature = "mnemonic")] 3 | use mnemonic::{Language, Mnemonic}; 4 | 5 | #[cfg(feature = "rand")] 6 | pub fn random_bytes(rng: &mut R) -> [u8; S] 7 | where 8 | R: rand_core::CryptoRng + rand_core::RngCore, 9 | { 10 | let mut bytes = [0u8; S]; 11 | rng.fill_bytes(&mut bytes); 12 | bytes 13 | } 14 | 15 | #[cfg(feature = "rand")] 16 | pub fn gen_phrase(rng: &mut R, lang: mnemonic::Language) -> mnemonic::Mnemonic 17 | where 18 | R: rand_core::CryptoRng + rand_core::RngCore, 19 | { 20 | let seed = random_bytes::<_, 32>(rng); 21 | let phrase = mnemonic::Mnemonic::from_entropy_in(lang, seed.as_ref()).expect("seed valid"); 22 | phrase 23 | } 24 | 25 | /// A simple pin credential that can be used to add some 26 | /// extra level of protection to seeds stored in vaults 27 | #[derive(Default, Copy, Clone)] 28 | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 29 | pub struct Pin(u16); 30 | 31 | macro_rules! seed_from_entropy { 32 | ($seed: ident, $pin: expr) => { 33 | #[cfg(feature = "util_pin")] 34 | let protected_seed = $pin.protect::<64>($seed); 35 | #[cfg(feature = "util_pin")] 36 | let $seed = &protected_seed; 37 | #[cfg(not(feature = "util_pin"))] 38 | let _ = &$pin; // use the variable to avoid warning 39 | }; 40 | } 41 | 42 | pub(crate) use seed_from_entropy; 43 | 44 | impl Pin { 45 | const LEN: usize = 4; 46 | 47 | #[cfg(feature = "util_pin")] 48 | pub fn protect(&self, data: &[u8]) -> [u8; S] { 49 | use hmac::Hmac; 50 | use pbkdf2::pbkdf2; 51 | use sha2::Sha512; 52 | 53 | let salt = { 54 | let mut s = [0; 10]; 55 | s.copy_from_slice(b"mnemonic\0\0"); 56 | let [b1, b2] = self.to_le_bytes(); 57 | s[8] = b1; 58 | s[9] = b2; 59 | s 60 | }; 61 | let mut seed = [0; S]; 62 | // using same hashing strategy as Substrate to have some compatibility 63 | // when pin is 0(no pin) we produce the same addresses 64 | let len = if self.eq(&0) { 65 | salt.len() - 2 66 | } else { 67 | salt.len() 68 | }; 69 | pbkdf2::>(data, &salt[..len], 2048, &mut seed); 70 | seed 71 | } 72 | } 73 | 74 | // Use 4 chars long hex string as pin. i.e. "ABCD", "1234" 75 | impl From<&str> for Pin { 76 | fn from(s: &str) -> Self { 77 | let l = s.len().min(Pin::LEN); 78 | let chars = s 79 | .chars() 80 | .take(l) 81 | .chain(iter::repeat('0').take(Pin::LEN - l)); 82 | Pin(chars 83 | .map(|c| c.to_digit(16).unwrap_or(0)) 84 | .enumerate() 85 | .fold(0, |pin, (i, d)| { 86 | pin | ((d as u16) << ((Pin::LEN - 1 - i) * Pin::LEN)) 87 | })) 88 | } 89 | } 90 | 91 | impl<'a> From> for Pin { 92 | fn from(p: Option<&'a str>) -> Self { 93 | p.unwrap_or("").into() 94 | } 95 | } 96 | 97 | impl From<()> for Pin { 98 | fn from(_: ()) -> Self { 99 | Self(0) 100 | } 101 | } 102 | 103 | impl From for Pin { 104 | fn from(n: u16) -> Self { 105 | Self(n) 106 | } 107 | } 108 | 109 | impl ops::Deref for Pin { 110 | type Target = u16; 111 | 112 | fn deref(&self) -> &Self::Target { 113 | &self.0 114 | } 115 | } 116 | 117 | #[test] 118 | fn pin_parsing() { 119 | for (s, expected) in [ 120 | ("0000", 0), 121 | // we only take the first 4 characters and ignore the rest 122 | ("0000001", 0), 123 | // non hex chars are ignored and defaulted to 0, here a,d are kept 124 | ("zasdasjgkadg", 0x0A0D), 125 | ("ABCD", 0xABCD), 126 | ("1000", 0x1000), 127 | ("000F", 0x000F), 128 | ("FFFF", 0xFFFF), 129 | ] { 130 | let pin = Pin::from(s); 131 | assert_eq!( 132 | *pin, expected, 133 | "(input:\"{}\", l:{:X} == r:{:X})", 134 | s, *pin, expected 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /sdk/js/examples/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cliente Virto SDK 8 | 98 | 99 | 100 | 101 |

Cliente Virto SDK

102 |

Este ejemplo muestra cómo utilizar el SDK de Virto para registrar y conectar usuarios mediante WebAuthn.

103 |
104 |
Datos de configuración
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 | 115 |
116 |
Registro de usuario
117 |
118 |
Registro en dos pasos:
119 | 120 | 121 |
122 |
123 | 124 |
125 |
Conexión de usuario
126 | 127 |
128 |
Conexión en dos pasos:
129 | 130 | 131 |
132 |
133 | 134 |
135 |
Firma de comandos
136 |
137 | 138 | 139 |
140 | 141 |
142 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /sdk/js/examples/server/src/storage/MongoDBStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from "../../../../src/storage/index"; 2 | import { MongoClient, Db, Collection } from 'mongodb'; 3 | 4 | /** 5 | * MongoDB implementation for server-side session storage 6 | * 7 | * Usage: 8 | * ```typescript 9 | * import { ServerSDK } from "@virto-sdk/js"; 10 | * import { MongoDBStorage } from "./storage/MongoDBStorage"; 11 | * 12 | * const mongoStorage = new MongoDBStorage({ 13 | * url: 'mongodb://localhost:27017', 14 | * dbName: 'virto_sessions' 15 | * }); 16 | * 17 | * const serverSDK = new ServerSDK({ 18 | * // ... other options 19 | * }, mongoStorage); 20 | * ``` 21 | */ 22 | export class MongoDBStorage implements IStorage { 23 | private client: MongoClient; 24 | private db: Db | null = null; 25 | private collection: Collection | null = null; 26 | private dbName: string; 27 | private collectionName: string; 28 | 29 | constructor(options: { 30 | url?: string; 31 | dbName?: string; 32 | collectionName?: string; 33 | clientOptions?: any; 34 | } = {}) { 35 | const url = options.url || 'mongodb://localhost:27017'; 36 | this.dbName = options.dbName || 'virto_sessions'; 37 | this.collectionName = options.collectionName || 'sessions'; 38 | 39 | this.client = new MongoClient(url, { 40 | maxPoolSize: 10, 41 | serverSelectionTimeoutMS: 5000, 42 | socketTimeoutMS: 45000, 43 | ...options.clientOptions 44 | }); 45 | 46 | this.client.on('error', (err: any) => console.error('MongoDB Client Error', err)); 47 | } 48 | 49 | async connect(): Promise { 50 | try { 51 | await this.client.connect(); 52 | this.db = this.client.db(this.dbName); 53 | this.collection = this.db.collection(this.collectionName); 54 | 55 | // Create index on key for better performance 56 | await this.collection.createIndex({ key: 1 }, { unique: true }); 57 | } catch (error) { 58 | // Connection will be retried automatically 59 | } 60 | } 61 | 62 | async disconnect(): Promise { 63 | try { 64 | await this.client.close(); 65 | } catch (error) { 66 | console.error('Error disconnecting from MongoDB:', error); 67 | } 68 | } 69 | 70 | async store(key: string, session: T): Promise { 71 | await this.connect(); 72 | if (!this.collection) { 73 | throw new Error('MongoDB collection not initialized'); 74 | } 75 | 76 | await this.collection.replaceOne( 77 | { key }, 78 | { 79 | key, 80 | data: session, 81 | createdAt: new Date(), 82 | updatedAt: new Date() 83 | }, 84 | { upsert: true } 85 | ); 86 | } 87 | 88 | async get(key: string): Promise { 89 | await this.connect(); 90 | if (!this.collection) { 91 | throw new Error('MongoDB collection not initialized'); 92 | } 93 | 94 | const document = await this.collection.findOne({ key }); 95 | return document ? document.data : null; 96 | } 97 | 98 | async getAll(): Promise { 99 | await this.connect(); 100 | if (!this.collection) { 101 | throw new Error('MongoDB collection not initialized'); 102 | } 103 | 104 | const documents = await this.collection.find({}).toArray(); 105 | return documents.map((doc: any) => doc.data); 106 | } 107 | 108 | async remove(key: string): Promise { 109 | await this.connect(); 110 | if (!this.collection) { 111 | throw new Error('MongoDB collection not initialized'); 112 | } 113 | 114 | const result = await this.collection.deleteOne({ key }); 115 | return result.deletedCount > 0; 116 | } 117 | 118 | async clear(): Promise { 119 | await this.connect(); 120 | if (!this.collection) { 121 | throw new Error('MongoDB collection not initialized'); 122 | } 123 | 124 | await this.collection.deleteMany({}); 125 | } 126 | } -------------------------------------------------------------------------------- /lib/libwallet/js/src/wallet.rs: -------------------------------------------------------------------------------- 1 | use js_sys::Uint8Array; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_wasm_bindgen::from_value; 4 | use wasm_bindgen::prelude::*; 5 | 6 | use libwallet::{vault::Simple, Signer, Wallet, Account}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub enum WalletConstructor { 10 | Simple(Option), 11 | } 12 | 13 | type SimpleVault = Simple; 14 | #[wasm_bindgen(inspectable)] 15 | pub struct JsWallet { 16 | phrase: String, 17 | wallet: Wallet, 18 | } 19 | 20 | #[wasm_bindgen] 21 | impl JsWallet { 22 | #[wasm_bindgen(constructor)] 23 | pub fn new(constructor: JsValue) -> Self { 24 | wasm_logger::init(wasm_logger::Config::default()); 25 | console_error_panic_hook::set_once(); 26 | 27 | let constructor: WalletConstructor = from_value(constructor).unwrap(); 28 | 29 | let (vault, phrase) = match constructor { 30 | WalletConstructor::Simple(phrase) => match phrase { 31 | Some(phrase) => { 32 | let vault = SimpleVault::from_phrase(&phrase); 33 | (vault, String::from(phrase.as_str())) 34 | } 35 | _ => { 36 | let (vault, mnemonic) = SimpleVault::generate_with_phrase(&mut rand_core::OsRng); 37 | (vault, mnemonic.into_phrase()) 38 | } 39 | }, 40 | }; 41 | 42 | JsWallet { 43 | phrase, 44 | wallet: Wallet::new(vault), 45 | } 46 | } 47 | 48 | #[wasm_bindgen(getter)] 49 | pub fn phrase(&self) -> String { 50 | self.phrase.clone() 51 | } 52 | 53 | #[wasm_bindgen] 54 | pub async fn unlock(&mut self, id: JsValue, credentials: JsValue) -> Result<(), JsValue> { 55 | let credentials: ::Credentials = 56 | if credentials.is_null() || credentials.is_undefined() { 57 | None 58 | } else { 59 | from_value(credentials).unwrap_or(None) 60 | }; 61 | 62 | let id: ::Id = 63 | if id.is_null() || id.is_undefined() { 64 | None 65 | } else { 66 | from_value(id).unwrap_or(None) 67 | }; 68 | 69 | 70 | self.wallet 71 | .unlock(id, credentials) 72 | .await 73 | .map_err(|e| JsError::new(&e.to_string()))?; 74 | 75 | Ok(()) 76 | } 77 | 78 | #[wasm_bindgen(js_name = getAddress)] 79 | pub fn get_address(&self) -> Result { 80 | if self.wallet.is_locked() { 81 | return Err(JsError::new( 82 | "The wallet is locked. You should unlock it first by using the .unlock() method", 83 | )); 84 | } 85 | let account = self.wallet.default_account().expect("it must be unlocked"); 86 | 87 | Ok(JsPublicAddress::new( 88 | account.public().as_ref().to_vec(), 89 | )) 90 | } 91 | 92 | #[wasm_bindgen] 93 | pub async fn sign(&self, message: &[u8]) -> Result, JsError> { 94 | if self.wallet.is_locked() { 95 | return Err(JsError::new( 96 | "The wallet is locked. You should unlock it first by using the .unlock() method", 97 | )); 98 | } 99 | 100 | let sig = self.wallet.sign(message).await.map_err(|e| JsError::new("Failed to sign message".into()))?; 101 | let is_verified = self 102 | .wallet 103 | .default_account() 104 | .expect("it must be unlocked") 105 | .verify(&message, &sig.as_ref()).await; 106 | 107 | if !is_verified 108 | { 109 | return Err(JsError::new("Message could not be verified")); 110 | } 111 | 112 | Ok(sig.as_ref().to_vec().into_boxed_slice()) 113 | } 114 | 115 | #[wasm_bindgen] 116 | pub async fn verify(&self, msg: &[u8], sig: &[u8]) -> Result { 117 | if self.wallet.is_locked() { 118 | return Err(JsError::new( 119 | "The wallet is locked. You should unlock it first by using the .unlock() method", 120 | )); 121 | } 122 | 123 | Ok(self.wallet.default_account().expect("it must be unlocked").verify(msg, sig).await) 124 | } 125 | } 126 | 127 | #[wasm_bindgen(inspectable)] 128 | pub struct JsPublicAddress { 129 | repr: Vec, 130 | } 131 | 132 | #[wasm_bindgen] 133 | impl JsPublicAddress { 134 | #[wasm_bindgen(constructor)] 135 | pub fn new(repr: Vec) -> Self { 136 | Self { repr } 137 | } 138 | 139 | #[cfg(feature = "hex")] 140 | #[wasm_bindgen(js_name = toHex)] 141 | pub fn to_hex(&self) -> JsValue { 142 | format!("0x{}", hex::encode(&self.repr)).into() 143 | } 144 | 145 | #[wasm_bindgen(getter)] 146 | pub fn repr(&self) -> Uint8Array { 147 | Uint8Array::from(self.repr.as_slice()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /components/input.js: -------------------------------------------------------------------------------- 1 | import { html, css } from "./utils.js"; 2 | import { globalStyles } from "./globalStyles.js"; 3 | 4 | const inputTp = html``; 5 | 6 | const inputCss = await css` 7 | :host { 8 | width: 100%; 9 | } 10 | wa-input::part(base) { 11 | box-sizing: border-box; 12 | line-height: 28px; 13 | padding: 1em; 14 | margin-top: 1em; 15 | border-radius: 12px; 16 | border: 1px solid var(--lightgreen); 17 | background: var(--extra-light-green); 18 | font-family: Outfit, sans-serif; 19 | } 20 | wa-input::part(base):focus { 21 | outline: 2px solid var(--green); 22 | } 23 | wa-input[invalid]::part(base) { 24 | outline: 2px solid #ff0000; 25 | } 26 | :host([disabled]) > wa-input::part(base) { 27 | opacity: 0.6; 28 | cursor: not-allowed; 29 | background-color: var(--grey-green); 30 | } 31 | .error-message { 32 | color: #ff0000; 33 | font-size: 0.875em; 34 | margin-top: 0.25em; 35 | } 36 | `; 37 | 38 | export class InputVirto extends HTMLElement { 39 | static TAG = "virto-input"; 40 | static formAssociated = true; 41 | #internals; 42 | 43 | static observedAttributes = [ 44 | "name", "type", "value", "label", "hint", "disabled", "placeholder", 45 | "readonly", "required", "pattern", "minlength", "maxlength", "min", 46 | "max", "step", "autocomplete", "autofocus" 47 | ]; 48 | 49 | constructor() { 50 | super(); 51 | const shadow = this.attachShadow({ mode: "open" }); 52 | shadow.append(inputTp.content.cloneNode(true)); 53 | this.shadowRoot.adoptedStyleSheets = [globalStyles, inputCss]; 54 | 55 | this.waInput = shadow.querySelector("wa-input"); 56 | this.errorMessage = shadow.appendChild(document.createElement("div")); 57 | this.errorMessage.className = "error-message"; 58 | this.#internals = this.attachInternals(); 59 | } 60 | 61 | connectedCallback() { 62 | this.updateWaInputAttributes(); 63 | this.setupEventForwarding(); 64 | this.waInput.addEventListener("input", this.#handleInput); 65 | this.waInput.addEventListener("change", this.#handleChange); 66 | this.waInput.addEventListener("blur", this.validateInput.bind(this)); 67 | if (this.hasAttribute("value")) this.value = this.getAttribute("value"); 68 | } 69 | 70 | attributeChangedCallback(name, oldValue, newValue) { 71 | if (oldValue === newValue || !this.waInput) return; 72 | if (name === "disabled") { 73 | this.waInput.disabled = newValue !== null; 74 | } else { 75 | this.waInput.setAttribute(name, newValue || ""); 76 | } 77 | if (name === "value") this.#syncValue(); 78 | } 79 | 80 | get value() { return this.waInput.value || ""; } 81 | set value(newValue) { 82 | this.waInput.value = newValue; 83 | this.setAttribute("value", newValue); 84 | this.#syncValue(); 85 | this.validateInput(); 86 | } 87 | 88 | #handleInput = (event) => { 89 | this.value = event.target.value; 90 | this.dispatchEvent(new CustomEvent("input", { detail: { value: this.value }, bubbles: true, composed: true })); 91 | }; 92 | 93 | #handleChange = (event) => { 94 | this.value = event.target.value; 95 | this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value }, bubbles: true, composed: true })); 96 | }; 97 | 98 | #syncValue() { 99 | this.#internals.setFormValue(this.value); 100 | } 101 | 102 | validateInput() { 103 | const validity = this.waInput.validity; 104 | let errorMessage = ""; 105 | 106 | if (validity.patternMismatch || validity.typeMismatch) { 107 | errorMessage = this.getAttribute("data-error-message") || "Please enter a valid value."; 108 | } else if (validity.valueMissing) { 109 | errorMessage = "This field is required."; 110 | } else if (validity.tooShort) { 111 | errorMessage = `Please enter at least ${this.getAttribute("minlength")} characters.`; 112 | } 113 | 114 | this.waInput.setCustomValidity(errorMessage); 115 | if (errorMessage) { 116 | this.waInput.setAttribute("invalid", ""); 117 | this.errorMessage.textContent = errorMessage; 118 | } else { 119 | this.waInput.removeAttribute("invalid"); 120 | this.errorMessage.textContent = ""; 121 | } 122 | } 123 | 124 | updateWaInputAttributes() { 125 | if (this.waInput) { 126 | Array.from(this.attributes).forEach((attr) => { 127 | if (InputVirto.observedAttributes.includes(attr.name)) { 128 | this.waInput.setAttribute(attr.name, attr.value); 129 | } 130 | }); 131 | } 132 | } 133 | 134 | setupEventForwarding() { 135 | const events = ["input", "change", "blur", "focus", "invalid"]; 136 | events.forEach((eventName) => { 137 | this.waInput.addEventListener(eventName, (event) => { 138 | this.dispatchEvent(new CustomEvent(eventName, { detail: event.detail, bubbles: true, composed: true })); 139 | }); 140 | }); 141 | } 142 | 143 | get form() { return this.#internals.form; } 144 | get name() { return this.getAttribute("name"); } 145 | } 146 | 147 | if (!customElements.get(InputVirto.TAG)) { 148 | customElements.define(InputVirto.TAG, InputVirto); 149 | } --------------------------------------------------------------------------------