Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ Part of [Lumio](https://github.com/lumio-network) — an open-source cooperative

| Package | Name | Responsibility |
| -------------- | ---------------- | -------------------------------------------------------------------------- |
| `packages/shared` | `@lumio/shared` | Shared types + utils used by `sdk`, `ui`, and every `lumio-app` package. |
| `packages/sdk` | `@lumio/sdk` | Contract client — one module per Soroban contract, wrapping RPC calls. |
| `packages/ui` | `@lumio/ui` | Shared component library + design tokens (the "Ledger of Light" system). |
| `packages/shared` | `@lumio/shared` | Shared types + utils used by `sdk`, `ui`, and every `lumio-app` package. · [README](./packages/shared/README.md) |
| `packages/sdk` | `@lumio/sdk` | Contract client — one module per Soroban contract, wrapping RPC calls. · [README](./packages/sdk/README.md) |
| `packages/ui` | `@lumio/ui` | Shared component library + design tokens (the "Ledger of Light" system). · [README](./packages/ui/README.md) |

`sdk` and `ui` depend on `shared`. All three are consumed by `lumio-app`.

Expand Down
68 changes: 62 additions & 6 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,72 @@ pnpm add @lumio/sdk

## Usage

### Construct a client

```ts
import { LumioClient } from "@lumio/sdk";
import { LumioClient, type ContractIds } from "@lumio/sdk";
import { NETWORKS } from "@lumio/shared";

const contractIds: ContractIds = {
treasury: "CTREASURY000000000000000000000000000000000000000000000000",
governance: "CGOVERNANCE0000000000000000000000000000000000000000000000",
dividends: "CDIVIDENDS00000000000000000000000000000000000000000000000",
voting: "CVOTING0000000000000000000000000000000000000000000000000",
};

const lumio = new LumioClient({ network: NETWORKS.testnet, contractIds });
```

Replace each contract id with the real deployed address for your target network.
`NETWORKS.testnet`, `NETWORKS.futurenet`, and `NETWORKS.mainnet` are exported from
`@lumio/shared`.

const lumio = new LumioClient({ network, contractIds });
const total = await lumio.treasury.total();
### Read data (scaffold — returns mock values)

```ts
const total = await lumio.treasury.total(); // 0n
const balance = await lumio.treasury.balanceOf(memberAddress); // 0n
const count = await lumio.governance.proposalCount(); // 0
const proposal = await lumio.governance.getProposal(1); // null
const pool = await lumio.dividends.pool(); // 0n
const shares = await lumio.dividends.listShares(); // []
const tally = await lumio.voting.tally(1); // { yes: 0, no: 0, abstain: 0 }
```

Individual clients (`TreasuryClient`, `GovernanceClient`, `DividendsClient`, `VotingClient`) are
also exported, along with `ContractClient`, `NotImplementedError`, and every type from
[`@lumio/shared`](https://www.npmjs.com/package/@lumio/shared).
### Write methods (scaffold — throws `NotImplementedError`)

```ts
import { NotImplementedError } from "@lumio/sdk";

try {
await lumio.treasury.deposit(memberAddress, 10_000_000n);
} catch (e) {
if (e instanceof NotImplementedError) {
// Expected in scaffold phase — Soroban RPC wiring is not yet implemented.
}
}
```

### Individual contract clients

Each client is also exported for direct use:

```ts
import {
TreasuryClient,
GovernanceClient,
DividendsClient,
VotingClient,
} from "@lumio/sdk";

const treasury = new TreasuryClient({ contractId: contractIds.treasury, network: NETWORKS.testnet });
```

### Types

`@lumio/sdk` re-exports every type from `@lumio/shared` — `Member`, `Contribution`, `Proposal`,
`Vote`, `Dividend`, `Address`, `Amount`, `ContractName`, `NetworkConfig`, and more — so app code
only needs a single import source.

## License

Expand Down
60 changes: 60 additions & 0 deletions packages/shared/src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
formatAmount,
parseAmount,
approvalRate,
getNetwork,
isValidAddress,
InvalidAmountError,
} from "./utils";
import { NETWORKS } from "./types";

describe("truncateAddress", () => {
it("shortens long addresses with an ellipsis", () => {
Expand Down Expand Up @@ -158,3 +161,60 @@ describe("approvalRate", () => {
expect(approvalRate(3, 1)).toBe(75);
});
});

describe("getNetwork", () => {
it("returns the matching NetworkConfig for testnet", () => {
const config = getNetwork("testnet");
expect(config.rpcUrl).toBe("https://soroban-testnet.stellar.org");
expect(config.networkPassphrase).toBe("Test SDF Network ; September 2015");
});

it("returns the matching NetworkConfig for futurenet", () => {
const config = getNetwork("futurenet");
expect(config.rpcUrl).toBe("https://rpc-futurenet.stellar.org");
});

it("returns the matching NetworkConfig for mainnet", () => {
const config = getNetwork("mainnet");
expect(config.rpcUrl).toBe("https://mainnet.sorobanrpc.com");
});

it("returns the same object reference as NETWORKS[name]", () => {
expect(getNetwork("testnet")).toBe(NETWORKS.testnet);
expect(getNetwork("mainnet")).toBe(NETWORKS.mainnet);
});
});

describe("isValidAddress", () => {
it("accepts a well-formed G... public key (56 chars, base32)", () => {
expect(isValidAddress("GABC2DEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV")).toBe(true);
});

it("accepts a well-formed C... contract id (56 chars, base32)", () => {
expect(isValidAddress("CTREASURY000000000000000000000000000000000000000000000000")).toBe(false); // 0 not base32
expect(isValidAddress("CTREASURYBCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOP")).toBe(true);
});

it("rejects an address that is too short", () => {
expect(isValidAddress("GABCDE")).toBe(false);
});

it("rejects an address that is too long", () => {
expect(isValidAddress("G" + "A".repeat(56))).toBe(false);
});

it("rejects an address starting with an invalid prefix", () => {
expect(isValidAddress("XABC2DEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV")).toBe(false);
expect(isValidAddress("SABC2DEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV")).toBe(false);
});

it("rejects an address containing non-base32 characters", () => {
// '0', '1', '8', '9' are not valid base32 chars
expect(isValidAddress("G0BC2DEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV")).toBe(false);
expect(isValidAddress("GABC2DEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTU1")).toBe(false);
});

it("rejects an empty string", () => {
expect(isValidAddress("")).toBe(false);
});
});
27 changes: 26 additions & 1 deletion packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Address, Amount } from "./types";
import type { Address, Amount, NetworkConfig, NetworkName } from "./types";
import { NETWORKS } from "./types";

/** Number of decimal places Stellar uses for native amounts. */
export const STELLAR_DECIMALS = 7;
Expand Down Expand Up @@ -103,3 +104,27 @@ export function approvalRate(yes: number, no: number, decimalPlaces = 1): number
const factor = 10 ** decimalPlaces;
return Math.round((yes / total) * 100 * factor) / factor;
}

/**
* Resolve a {@link NetworkConfig} by its well-known name.
*
* @example getNetwork("testnet") // { rpcUrl: "https://soroban-testnet.stellar.org", ... }
*/
export function getNetwork(name: NetworkName): NetworkConfig {
return NETWORKS[name];
}

/**
* A lightweight format guard for Stellar addresses (public keys and contract ids).
*
* Returns `true` when `value` matches the shape of a well-formed Stellar strkey:
* - 56 characters long
* - Starts with `G` (ed25519 public key) or `C` (contract id)
* - Contains only base32 characters (A–Z and 2–7)
*
* **Limitation:** this is a shape check only — it does not verify the Stellar
* strkey checksum or confirm the address exists on-chain.
*/
export function isValidAddress(value: string): boolean {
return /^[GC][A-Z2-7]{55}$/.test(value);
}
Loading