client.addressBook manages saved transfer destinations, tags, whitelist views, and recent
counterparties. Every method is authenticated and account-scoped: inputs accept an optional account field ("main", "active", or { subaccountId }) that selects the root or subaccount book. See Account scoping for how the default
resolves. Creates, updates, and deletes need the manage-address-book policy action. A key without that action gets AuthenticationError on
writes. Reads still work.
Destinations come in two kinds: external (an on-chain address on a specific chain) and internal (another Polyester account by smart-account address). These feed the internal transfer and withdrawal flows.
Methods
| Method | Summary |
|---|---|
listBooks | List the root and subaccount books visible to the caller. |
listEntries | List saved entries for the resolved scope. |
createEntry | Save an external or internal destination. |
updateEntry | Patch an entry's label, note, or tags (requires revision). |
deleteEntry | Delete one saved entry. |
copyEntry | Copy an entry into another visible book. |
createTag / updateTag / deleteTag | Manage tags for organizing entries. |
listTransferCounterparties | List recent counterparties, including unsaved ones. |
listTransferDestinations | List saved and whitelisted destinations for transfer flows. |
listInternalTransferWhitelistEntries | List internal-transfer whitelist entries. |
getWithdrawWhitelistView | Read withdrawal whitelist requirements and active entries. |
getView | Fetch the composite dashboard view in one call. |
subscribeViewInvalidations | Stream signals to refetch getView. |
listBooks(options?)
Returns the root and subaccount address books visible to the caller, with the caller's role and display metadata for each.
const books = await client.addressBook.listBooks();
for (const book of books) {
console.log(book.label, book.callerRole, book.smartAccountAddress);
}Entries
listEntries(input?, options?)
Returns saved entries for the resolved scope as { entries, nextPageToken }. Filter by kind ("external" or "internal") and page with limit and pageToken.
const { entries } = await client.addressBook.listEntries({ kind: "external" });
for (const entry of entries) console.log(entry.label, entry.kind);createEntry(input, options?)
Saves a destination and returns the created AddressBookEntry, or null. The entry field is a
tagged union: external needs polychainChainId and address; internal needs smartAccountAddress. Attach existing tags with tagIds, or create tags inline with newTags.
const existingTag = await client.addressBook.createTag({ name: "treasury" });
if (!existingTag) throw new Error("createTag returned null");
// External on-chain destination
const entry = await client.addressBook.createEntry({
label: "Cold wallet",
entry: { kind: "external", polychainChainId: 1, address: "0xabc..." },
tagIds: [existingTag.tagId],
});
if (!entry) throw new Error("createEntry returned null");
// Internal Polyester account
await client.addressBook.createEntry({
label: "Ops subaccount",
entry: { kind: "internal", smartAccountAddress: "0xdef..." },
newTags: [{ name: "internal", color: "#22c55e" }],
});Both entry methods accept at most 10 newTags. Each tag name is trimmed and must contain 1 to 48
characters. An optional color is trimmed, may contain up to 32 characters, and defaults to "".
updateEntry(input, options?)
Patches the provided label, note, or tag set for an entry, keyed by addressBookEntryId.
Pass the latest revision returned by getView, createEntry, or updateEntry; the returned entry
contains the next revision. Omitted fields are unchanged.
await client.addressBook.updateEntry({
addressBookEntryId: entry.addressBookEntryId,
expectedRevision: entry.revision,
label: "Cold wallet (rotated)",
note: "moved 2026-06",
tagIds: [existingTag.tagId],
newTags: [{ name: "rotated", color: "#6366f1" }],
});newTags creates tags and updates the entry atomically. With newTags alone, the entry keeps its
existing tag IDs and adds the created tags. Supplying both tagIds and newTags replaces the tag
set with those existing IDs plus the newly created tags.
If another writer changed the entry first, the SDK throws RevisionConflictError. Refetch the
address-book view and ask the user to review their draft against the latest entry; do not retry the
same update automatically.
deleteEntry(input, options?)
Deletes one saved entry from the selected book.
await client.addressBook.deleteEntry({ addressBookEntryId: entry.addressBookEntryId });copyEntry(input, options?)
Copies an entry into another visible book, targeting targetSubaccountId. Returns the new entry.
await client.addressBook.copyEntry({
addressBookEntryId: entry.addressBookEntryId,
targetSubaccountId,
});Tags
createTag, updateTag, and deleteTag manage the tags used to organize entries. Deleting a tag
detaches it from any entries.
const tag = await client.addressBook.createTag({ name: "treasury", color: "#4f46e5" });
if (!tag) throw new Error("createTag returned null");
await client.addressBook.updateTag({ tagId: tag.tagId, name: "treasury", color: "#6366f1" });
await client.addressBook.deleteTag({ tagId: tag.tagId });Transfer flows
listTransferCounterparties(input?, options?)
Returns recent transfer counterparties, including unsaved destinations, as { counterparties, truncated }. Each carries direction, kind, use count, and first/last seen
timestamps. Filter by direction, kind, and limit.
const { counterparties } = await client.addressBook.listTransferCounterparties({
direction: "withdrawTo",
});listTransferDestinations(input?, options?)
Returns saved and whitelisted destinations available for transfer flows as { destinations, nextPageToken }. Each destination reports whether it is saved and whitelisted, so this is the list to render in a "send to" picker.
const { destinations } = await client.addressBook.listTransferDestinations();
const ready = destinations.filter((d) => d.whitelisted);listInternalTransferWhitelistEntries(input?, options?)
Returns internal-transfer whitelist entries for the resolved scope as { entries, nextPageToken },
including target account metadata and resolution status.
const { entries } = await client.addressBook.listInternalTransferWhitelistEntries();getWithdrawWhitelistView(input?, options?)
Returns whether external and internal withdrawal whitelists are required, plus the active mirrored
external whitelist entries, or null when no view is returned.
const view = await client.addressBook.getWithdrawWhitelistView();
if (view?.externalWhitelistRequired) {
console.log(view.activeEntries);
}The composite view
getView(input?, options?)
Fetches the combined dashboard view in one call: books, saved entries, recentDestinations, tags, and withdrawWhitelist status. This is the canonical aggregate for rendering an address
book screen. Pass minimumViewRevision from an invalidation event to wait until the server view
has reached that revision.
const view = await client.addressBook.getView();
console.log(view.entries?.external.length, view.tags?.length);view.viewRevision is a decimal revision string. Use it as minimumViewRevision after an
invalidation instead of accepting a view older than the event.
subscribeViewInvalidations(input)
Streams scoped invalidation signals for a root account and returns an idempotent unsubscribe
function. Takes a rootAccountPublicId plus the standard handler fields. See the realtime client reference for the handler contract.
const unsubscribe = client.addressBook.subscribeViewInvalidations({
rootAccountPublicId,
onEvent: (event) => client.addressBook.getView({ minimumViewRevision: event.viewRevision }),
onError: (ctx) => console.error(ctx.channel, ctx.error),
});getView call, and do not patch individual rows from the event payload.
Pass the event's viewRevision as minimumViewRevision so the read does not
return an older projection. The event's scope and invalidatedAt are for
diagnostics and ordering.Account scopes
Entries, tags, and invalidation events carry an optional scope typed as AddressBookAccountScope, a union keyed by scopeType:
import type { AddressBookAccountScope } from "@polyester/sdk";
type AddressBookAccountScope =
| { scopeType: "root"; rootAccountId: string }
| { scopeType: "subaccount"; rootAccountId: string; subaccountId: string }
| { scopeType: "unspecified"; rootAccountId: string; subaccountId?: string };Narrow on scopeType before reading subaccountId. Root scopes have no subaccountId.
Related
- Internal transfers for sending to saved internal accounts.
- Withdrawals for sending to external destinations.
- Account scoping for how
accountresolves. - Realtime client for the streaming handler contract.