# Address book

Manage saved transfer destinations, tags, whitelists, and recent counterparties per account book.

`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](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances) 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](https://testnet.polyester.com/docs/sdk/typescript/reference/internal-transfers) and [withdrawal](https://testnet.polyester.com/docs/sdk/typescript/reference/withdrawals) 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.

```ts
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`.

```ts
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`.

```ts
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.

```ts
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.

```ts
await client.addressBook.deleteEntry({ addressBookEntryId: entry.addressBookEntryId });
```

### `copyEntry(input, options?)`

Copies an entry into another visible book, targeting `targetSubaccountId`. Returns the new entry.

```ts
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.

```ts
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`.

```ts
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.

```ts
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.

```ts
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.

```ts
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.

```ts
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](https://testnet.polyester.com/docs/sdk/typescript/reference/realtime) for the handler contract.

```ts
const unsubscribe = client.addressBook.subscribeViewInvalidations({
	rootAccountPublicId,
	onEvent: (event) => client.addressBook.getView({ minimumViewRevision: event.viewRevision }),
	onError: (ctx) => console.error(ctx.channel, ctx.error),
});
```

> **Subscribe, then refetch**
>
> Invalidation events signal that the view changed; they do not carry the new data. Use them only to trigger a fresh `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`:

```ts
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](https://testnet.polyester.com/docs/sdk/typescript/reference/internal-transfers) for sending to saved internal accounts.
- [Withdrawals](https://testnet.polyester.com/docs/sdk/typescript/reference/withdrawals) for sending to external destinations.
- [Account scoping](https://testnet.polyester.com/docs/sdk/typescript/guides/accounts-and-balances) for how `account` resolves.
- [Realtime client](https://testnet.polyester.com/docs/sdk/typescript/reference/realtime) for the streaming handler contract.
