@misofm/partyos
v0.8.0
Published
A sui-effect extension for the PartyOS object model (Party identity, admin caps, and group membership) on Sui.
Maintainers
Readme
@misofm/partyos
A sui-effect extension for the
PartyOS object model on Sui: the Party object, its PartyAdminCap, and
consent-based group membership.
@misofm/partyosis the object model. Everything Miso offers on top of it is platform.
A Party's profile, media, roles, tags, genres, CTAs, platform links, and wallet are
extensions someone chose to offer, so they ship from
@misofm/platform, which depends on
this package. Which Move package generates into which SDK is decided by
sui-codegen.config.ts in the misofm/sdks repo.
Install
bun add @misofm/partyos @unconfirmed/sui-effect effect @mysten/sui @mysten/bcsPeer dependencies: @unconfirmed/sui-effect@>=0.3.0 <0.4.0, effect@>=4.0.3 <4.1,
@mysten/sui@^2.29.0, @mysten/bcs@^2.1.1.
Use
Promise consumers: client.$extend
partyos() registers client.partyos, built once from the client's own
transport (one connection, one chain-identifier check shared with everything
else on the client). Every rejection is the same tagged error instance an
Effect caller would have caught, so a Promise consumer can still switch on
_tag:
import { SuiGrpcClient } from "@mysten/sui/grpc";
import { Transaction } from "@mysten/sui/transactions";
import { partyos, PartyNotFound } from "@misofm/partyos";
import { ObjectId } from "@unconfirmed/sui-effect";
const client = new SuiGrpcClient({ network: "testnet", baseUrl }).$extend(partyos());
try {
const party = await client.partyos.getPartyById(ObjectId.make("0x..."));
const groups = await client.partyos.getMemberships(party.id);
} catch (error) {
if (error instanceof PartyNotFound) {
// error.partyId
}
throw error;
}
// Recipe fragments are synchronous even on the Promise face (`warm` builds
// the runtime inside `register`, so `tx` is real immediately).
const tx = new Transaction();
client.partyos.tx.createIndividualParty({ name: "Ada", recipient: "0x..." })(tx);
await client.partyos.dispose();Effect consumers: the service directly
import { Effect, Layer } from "effect";
import { Sui, SuiCore } from "@unconfirmed/sui-effect";
import { Partyos } from "@misofm/partyos";
const program = Effect.gen(function* () {
const partyos = yield* Partyos;
const party = yield* partyos.getPartyById(partyId);
const groups = yield* partyos.getMemberships(party.id);
return { party, groups };
}).pipe(
Effect.catchTag("partyos/PartyNotFound", (error) => Effect.succeed({ party: null, groups: [], missing: error })),
);
await Effect.runPromise(
program.pipe(
Effect.provide(Partyos.layer()), // or `.layerConfig` / `.layerTest()`
Effect.provide(Sui.layerNoDeps),
Effect.provide(SuiCore.layerFromClient(client)),
),
);Every member's requirement channel is empty (R = never once you have a
Partyos): the layer captures Sui once and provides it to every member
internally.
PTB fragments
partyos.tx.* (or the free functions in ./transactions, each taking
partyPackageId explicitly) are Recipes — (tx: Transaction) => void — so
a consumer composes PartyOS's commands with other extensions' fragments into
one transaction and submits once:
import { Transaction } from "@mysten/sui/transactions";
const tx = new Transaction();
client.partyos.tx.setName({ partyId, capId, name: "New name" })(tx);
someOtherExtension.claim(...)(tx);Free functions
derivePartyAdminCapId and partyType (in ./queries) are pure and
synchronous — no Sui required — and stay exported for callers that only
need them:
import { derivePartyAdminCapId } from "@misofm/partyos/queries";
const capId = derivePartyAdminCapId(partyId, packageId);Event decoding
Use the pure decoders from @misofm/partyos/events after routing an event to
its known on-chain type:
import { partyEventParsers } from "@misofm/partyos/events";
const rawEventBcs: Uint8Array = getRawEventBytes();
const event = partyEventParsers.core.partyCreated(rawEventBcs);
// event.kind is 0 (individual) or 1 (group)
// event.created_at_ms and event.created_epoch are decimal stringspartyEventParsers targets the v1 PartyOS event ABI and preserves generated
snake_case fields. partyCreated is emitted when share makes the newly created
party public, so its snapshot contains the final state at the atomic share
transaction's execution epoch, along with the transaction sender as creator.
Its canonical events are partyCreated, partyNameSet,
partyGroupInviteCreated, partyGroupMembershipAccepted,
partyGroupInviteDeclined, partyGroupInviteRevoked,
partyGroupMembershipLeft, and partyGroupMembershipRemoved. u64 values
are decimal strings, and removed_since_epoch is string | null, preserving
the difference between an absent value and epoch "0". The registry does not
discover an event type or verify its deployment; callers choose the matching
codec after routing. Generated codecs, including the event codecs, remain
available from contracts.party.
Errors
| Member | Fails with |
|---|---|
| getPartyById | PartyNotFound (missing or deleted), DecodeError (not a Party of this deployment), TransportError |
| getPartiesByIds | TransportError (per-item failures — PartyNotFound, DecodeError, ObjectUnavailable — come back as a Result per distinct id) |
| getMemberships / getPendingInvites / getPendingMemberships | DecodeError, TransportError |
| isMember | TransportError |
| Partyos.layer / layerConfig | PartyosDeploymentError (an explicit manifest did not validate, or this release bundles no deployment for the client's network), ConfigError (layerConfig only) |
Every error is a Schema.TaggedError declaring outcome: "applied" | "not_applied" | "unknown"
(sui-effect's SuiError.outcome and Script.exitCode read it). Recover with
Effect.catchTag("partyos/PartyNotFound", ...) (the tag is prefixed with the
package name, per docs/extensions.md §2) — never an isNotFound(error)
predicate. ./errors also exports PartyReadError and PartyBatchItemError,
the two union aliases above.
Deployment manifest
PARTYOS_DEPLOYMENTS bundles the verified testnet deployment: a single key, partyos.
Pass partyos({ deployment, chainId }) for another network — chainId is required
outside mainnet/testnet because partyos() registers warm (see "Promise
consumers" above): without it, client.$extend(partyos({ deployment })) throws a plain
Error (from sui-effect's warm registration) synchronously, from inside
$extend itself, rather than rejecting the first call. A manifest with any other shape is rejected before
a Move target is constructed (assertPartyDeployment/normalizePartyDeployment
throw synchronously; validatePartyDeployment is the Effect-returning counterpart, now
failing with PartyosDeploymentError, for callers composing a config-loading pipeline
out of Effects). Partyos.layerConfig reads PARTYOS_PACKAGE_ID from the environment;
unset, it falls back to the bundled manifest for the client's network.
Move upgrades. PartyDeployment carries an optional typeOrigin, the
package the party module's types were first published in — what
appears inside pkg::party::Party, pkg::party::MembershipKey, and every
other type name this package compares against. It defaults to partyos
(the package tx targets for moveCalls) and that default is correct until
partyos is upgraded: an upgrade gives the package a new id for calls and
leaves every type name pointing at the one it was first published under. Set
typeOrigin explicitly once that happens, so getPartyById, the membership
reads and every dynamic-field tag comparison keep checking the type that
actually exists on chain, instead of failing every read with DecodeError
the moment partyos no longer matches the id baked into the original
publish.
Testing
@misofm/partyos/testing ships PARTYOS_TEST_DEPLOYMENT, fakeParty(...) (a
Party object with real BCS content), and fakeMembershipField(...) (a
MembershipKey / PendingInviteKey / PendingMembershipKey dynamic-field
entry) for building a FakeScript against @unconfirmed/sui-effect/testing's harness —
layerExtensionTest(Partyos.layerTest(), script) — with no network. See
tests/Partyos.test.ts in this repo.
Migrating from PartyosClient
PartyosClient, PartyProtocolClient and bindModulePackage are removed —
they held either a rebuilt Sui.layerNoDeps per call or a Layer.orDieed
NetworkMismatch, so a compatibility shim would have been dishonest about
its own failure modes as well as pointless: platform's own call sites are
adapted in its conversion (misofm/sdks#35), not by a shim here. Only two
source-compatible aliases remain, both @deprecated: TxThunk (Recipe,
in ./transactions) and PartyNotFoundError (PartyNotFound, in
./errors) — the latter's runtime _tag is the new
"partyos/PartyNotFound", not the string "PartyNotFoundError" a
predecessor caller may have matched on.
Subpaths
@misofm/partyos (everything), /client (Partyos, partyos), /deployments,
/errors, /events, /queries, /testing, /transactions, /types,
/contracts (curated bindings), /contracts/* (raw generated modules).
