Skip to content
Open
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
10 changes: 10 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/intercom.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,117 @@ Intercom is a single long-running Pear process that participates in three distin

---
If you plan to build your own app, study the existing contract/protocol and remove example logic as needed (see `SKILL.md`).

## Competition App: InterSplit (P2P Expense Splitter)
InterSplit is a sidechannel-native shared expense ledger for teams, friends, and co-travelers.

What it does:
- Tracks shared expenses in any sidechannel room.
- Computes per-member balances (who owes vs who should receive).
- Produces a minimal settlement plan (debtor -> creditor payments).
- Syncs entries peer-to-peer over Intercom sidechannels (no central server).
- Persists room snapshots into contract state for recovery after restarts.
- Exports settlement plans in one shot (`text`, `json`, or `csv`).

Terminal commands:
- `/expense_add --channel "<name>" --payer "<name>" --amount "<n>" --split "a,b,c" [--note "<text>"]`
- `/expense_list --channel "<name>"`
- `/expense_balance --channel "<name>"`
- `/expense_clear --channel "<name>"`
- `/expense_persist --channel "<name>" [--sim 1]`
- `/expense_restore --channel "<name>" [--confirmed 1|0] [--replace 1]`
- `/expense_export --channel "<name>" [--format text|json|csv]`

SC-Bridge JSON commands:
- `expense_add` with `channel`, `payer`, `amount`, `split`, optional `note`
- `expense_list` with `channel`
- `expense_balance` with `channel`
- `expense_clear` with `channel`
- `expense_persist` with `channel`, optional `sim`
- `expense_restore` with `channel`, optional `confirmed`, `replace`
- `expense_export` with `channel`, optional `format`

Web frontend (no terminal command entry needed):
1. Start Intercom with SC-Bridge enabled:
- `pear run . --peer-store-name demo --msb-store-name demo-msb --subnet-channel intersplit-demo --sidechannels trip-nyc --sc-bridge 1 --sc-bridge-token YOUR_TOKEN`
2. Start UI server:
- `npm run ui`
3. Open:
- `http://127.0.0.1:5070`
4. In the UI:
- Enter WS URL (`ws://127.0.0.1:49222`), token, and channel.
- Click `Connect`, then `Join`, then use Chat/Expense controls.
- `Persist` can take 10-60s depending on validator/network latency.
- `Restore` in UI reads local node view first (`confirmed=0`) for faster feedback.
- `Local node view` means files under `stores/<peer-store-name>/...` on your machine, not browser localStorage.
- Assistant prompt accepts simple commands like:
- `add alice 30 split alice,bob note dinner`
- `balance`
- `persist`
- `restore`
- `export text`

Frontend tutorial (end-to-end):
1. Start Intercom backend in terminal A:
```powershell
cd C:\Users\user\Documents\Emma\intercom
$env:PATH="$env:APPDATA\npm;$env:APPDATA\pear\bin;$env:PATH"
pear run . --peer-store-name demo2 --msb-store-name demo2-msb --subnet-channel intersplit-demo --sidechannels trip-nyc --sc-bridge 1 --sc-bridge-token mysecret123
```
2. Start frontend server in terminal B:
```powershell
cd C:\Users\user\Documents\Emma\intercom
npm run ui
```
3. Open browser at `http://127.0.0.1:5070`.
4. In the UI Connection card:
- WS URL: `ws://127.0.0.1:49222`
- Token: `mysecret123` (or your chosen token)
- Channel: `trip-nyc`
- Click `Connect`, `Join`, `Subscribe`.
5. In the UI, add expense records:
- Form mode: fill payer/amount/split/note and click `Add Expense`.
- Assistant mode: `add alice 30 split alice,bob note dinner`
6. Click `Balance` and verify expected output:
- `alice: +15.00`
- `bob: -15.00`
- settlement `bob -> alice: 15.00`
7. Click `Persist` once and wait until a tx hash appears in Live Feed.
8. Click `Export Text` to generate a copyable settlement summary.
9. Optional restart proof:
- Stop peer with `/exit` in terminal A.
- Start the same command again using the same store names (`demo2`, `demo2-msb`).
- In UI click `Restore` then `Balance`.
- Live Feed will show `source=contract` or `source=local`.

Optional two-peer chat verification:
1. Start peer A:
- `--peer-store-name demoA --msb-store-name demoA-msb --sc-bridge-port 49222 --sc-bridge-token tokenA`
2. Start peer B:
- `--peer-store-name demoB --msb-store-name demoB-msb --subnet-bootstrap <peer-writer-key-from-peer-A> --sc-bridge-port 49223 --sc-bridge-token tokenB`
3. Open two UI tabs:
- Tab A -> `ws://127.0.0.1:49222` / `tokenA`
- Tab B -> `ws://127.0.0.1:49223` / `tokenB`
4. Join + subscribe on both tabs to `trip-nyc`.
5. Send a chat message in Tab A; Tab B should receive `sidechannel_message`.

Contract persistence keys:
- `expense/room/<channel>` stores room snapshots (`events`) and update metadata.
- Local fallback snapshot file per peer store:
- `stores/<peer-store-name>/expense-split.snapshots.json`
- UI/CLI restore falls back to this local file if contract state is not yet confirmed.

Quick 60-second demo:
1. Peer A and Peer B join the same sidechannel, e.g. `trip-nyc`.
2. Add two expenses:
- `/expense_add --channel "trip-nyc" --payer "alice" --amount "30" --split "alice,bob" --note "dinner"`
- `/expense_add --channel "trip-nyc" --payer "bob" --amount "10" --split "alice,bob" --note "snacks"`
3. View settlement:
- `/expense_balance --channel "trip-nyc"`
4. Persist:
- `/expense_persist --channel "trip-nyc"`
5. Export ready-to-share settlement:
- `/expense_export --channel "trip-nyc" --format text`

## Trac Address (Payout)
- `trac1j8wqd88yhnssf74uzrpp5kvwmwdr6jnl42yxluldq893mjxvtf3s8hsyrq`
69 changes: 69 additions & 0 deletions contract/contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ class SampleContract extends Contract {
key : { type : "string", min : 1, max: 256 }
}
});
this.addSchema('expenseUpsertRoom', {
value : {
$$strict : true,
$$type: "object",
op : { type : "string", min : 1, max: 128 },
channel : { type : "string", min : 1, max: 128 },
snapshot : { type : "any" }
}
});
this.addSchema('expenseDeleteRoom', {
value : {
$$strict : true,
$$type: "object",
op : { type : "string", min : 1, max: 128 },
channel : { type : "string", min : 1, max: 128 }
}
});
this.addSchema('expenseReadRoom', {
value : {
$$strict : true,
$$type: "object",
op : { type : "string", min : 1, max: 128 },
channel : { type : "string", min : 1, max: 128 }
}
});

// now we are registering the timer feature itself (see /features/time/ in package).
// note the naming convention for the feature name <feature-name>_feature.
Expand Down Expand Up @@ -235,6 +260,50 @@ class SampleContract extends Contract {
const currentTime = await this.get('currentTime');
console.log('currentTime:', currentTime);
}

_expenseRoomKey(channel){
return 'expense/room/' + channel;
}

async expenseUpsertRoom(){
const channel = String(this.value?.channel || '').trim().toLowerCase();
if(channel.length === 0) return new Error('Channel is required.');

const snapshot = this.protocol.safeClone(this.value?.snapshot);
this.assert(snapshot !== null, new Error('Invalid snapshot payload.'));

const currentTime = await this.get('currentTime');
await this.put(this._expenseRoomKey(channel), {
channel,
snapshot,
updatedAt: currentTime ?? null,
updatedBy: this.address ?? null,
version: 1
});
}

async expenseDeleteRoom(){
const channel = String(this.value?.channel || '').trim().toLowerCase();
if(channel.length === 0) return new Error('Channel is required.');

const currentTime = await this.get('currentTime');
await this.put(this._expenseRoomKey(channel), {
channel,
snapshot: null,
deletedAt: currentTime ?? null,
deletedBy: this.address ?? null,
deleted: true,
version: 1
});
}

async expenseReadRoom(){
const channel = String(this.value?.channel || '').trim().toLowerCase();
if(channel.length === 0) return new Error('Channel is required.');

const value = await this.get(this._expenseRoomKey(channel));
console.log('expense room', channel, value);
}
}

export default SampleContract;
Loading