From 382e3d87e1990bc844c9b7003f07daa8eb2055a1 Mon Sep 17 00:00:00 2001 From: Jimi Date: Tue, 14 Jul 2026 21:34:17 +0800 Subject: [PATCH 1/3] feat(wallets): add Noir Wallet connector for Zcash Adds a NOIR_WALLET wallet option backed by the Noir Wallet browser extension (window.noirwallet). The connector registers Chain.Zcash with the UTXO toolbox, overriding getBalance/transfer/signMessage to delegate to the extension, which spends from the shielded pool and builds/signs transactions internally. Memo'd transfers throw wallet_noir_wallet_memo_not_supported since the extension cannot attach OP_RETURN data to transparent recipients, so OP_RETURN-based routes (Maya) are rejected explicitly while deposit-address routes (NEAR Intents) are fully supported. --- .changeset/noir-wallet-zcash.md | 9 ++ packages/helpers/src/modules/swapKitError.ts | 7 + packages/helpers/src/types/wallet.ts | 1 + packages/sdk/src/index.ts | 3 + .../dialogs/wallet-connect-dialog.tsx | 1 + packages/wallet-extensions/package.json | 6 + .../noir-wallet/__tests__/noir-wallet.test.ts | 127 ++++++++++++++++++ .../src/noir-wallet/index.ts | 81 +++++++++++ packages/wallet-extensions/src/types.ts | 9 ++ packages/wallets/package.json | 6 + packages/wallets/src/noir-wallet.ts | 1 + packages/wallets/src/types.ts | 2 + packages/wallets/src/utils.ts | 1 + playgrounds/vite/src/WalletPicker.tsx | 4 + 14 files changed, 258 insertions(+) create mode 100644 .changeset/noir-wallet-zcash.md create mode 100644 packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts create mode 100644 packages/wallet-extensions/src/noir-wallet/index.ts create mode 100644 packages/wallets/src/noir-wallet.ts diff --git a/.changeset/noir-wallet-zcash.md b/.changeset/noir-wallet-zcash.md new file mode 100644 index 000000000..bd38f581f --- /dev/null +++ b/.changeset/noir-wallet-zcash.md @@ -0,0 +1,9 @@ +--- +"@swapkit/helpers": minor +"@swapkit/wallet-extensions": minor +"@swapkit/wallets": minor +"@swapkit/sdk": minor +"@swapkit/ui": patch +--- + +Add Noir Wallet connector for Zcash (`connectNoirWallet`). Noir Wallet is a shielded-first Zcash browser extension; the connector delegates balance and transaction building to the extension and supports deposit-address swap routes (e.g. NEAR Intents). diff --git a/packages/helpers/src/modules/swapKitError.ts b/packages/helpers/src/modules/swapKitError.ts index 5113648de..d253ea80d 100644 --- a/packages/helpers/src/modules/swapKitError.ts +++ b/packages/helpers/src/modules/swapKitError.ts @@ -247,6 +247,13 @@ const errorCodes = { wallet_vultisig_asset_not_defined: 22103, wallet_vultisig_send_transaction_no_address: 22104, + /** + * Wallets - Noir Wallet + */ + wallet_noir_wallet_not_found: 22201, + wallet_noir_wallet_memo_not_supported: 22202, + wallet_noir_wallet_connection_failed: 22203, + /** * Wallets - Xaman */ diff --git a/packages/helpers/src/types/wallet.ts b/packages/helpers/src/types/wallet.ts index abc93e7cc..51e57bfa7 100644 --- a/packages/helpers/src/types/wallet.ts +++ b/packages/helpers/src/types/wallet.ts @@ -53,6 +53,7 @@ export enum WalletOption { LEDGER = "LEDGER", LEDGER_LIVE = "LEDGER_LIVE", METAMASK = "METAMASK", + NOIR_WALLET = "NOIR_WALLET", OKX = "OKX", OKX_MOBILE = "OKX_MOBILE", ONEKEY = "ONEKEY", diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 580e58697..0cced50b0 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -23,6 +23,7 @@ import { keplrWallet } from "@swapkit/wallets/keplr"; import { keystoreWallet } from "@swapkit/wallets/keystore"; import { ledgerWallet } from "@swapkit/wallets/ledger"; import { walletSelectorWallet } from "@swapkit/wallets/near-wallet-selector"; +import { noirWallet } from "@swapkit/wallets/noir-wallet"; import { okxWallet } from "@swapkit/wallets/okx"; import { onekeyWallet } from "@swapkit/wallets/onekey"; import { passkeysWallet } from "@swapkit/wallets/passkeys"; @@ -68,6 +69,7 @@ export { keplrWallet, keystoreWallet, ledgerWallet, + noirWallet, okxWallet, onekeyWallet, passkeysWallet, @@ -105,6 +107,7 @@ export const defaultWallets = { ...keplrWallet, ...keystoreWallet, ...ledgerWallet, + ...noirWallet, ...okxWallet, ...onekeyWallet, ...phantomWallet, diff --git a/packages/ui/src/react/components/dialogs/wallet-connect-dialog.tsx b/packages/ui/src/react/components/dialogs/wallet-connect-dialog.tsx index 4408f3c17..40b79c59f 100644 --- a/packages/ui/src/react/components/dialogs/wallet-connect-dialog.tsx +++ b/packages/ui/src/react/components/dialogs/wallet-connect-dialog.tsx @@ -79,6 +79,7 @@ export const availableChainsByWallet: Record undefined, + on: () => undefined, + removeListener: () => undefined, + request: ({ method, params }) => { + requests.push({ method, params }); + + switch (method) { + case "zcash_requestAccounts": + return Promise.resolve({ accounts: [], shielded: "zs1mock", transparent: TRANSPARENT_ADDRESS }); + case "zcash_getBalance": + return Promise.resolve({ + available: "1.42", + shielded: "1.5", + spendable: "1.45", + total: "1.6", + transparent: "0.1", + ...balance, + }); + case "zcash_sendTransaction": + return Promise.resolve("mocktxid123"); + case "zcash_signMessage": + return Promise.resolve({ + address: TRANSPARENT_ADDRESS, + pubkey: "02aa", + signature: "sigabc", + signingMode: "current", + }); + default: + return Promise.reject(new Error(`unexpected method ${method}`)); + } + }, + }, + }, + }; + + return requests; +} + +async function connectAndGetWallet() { + let addedWallet = null; + const connect = noirWallet.connectNoirWallet.connectWallet({ + addChain: (wallet) => { + addedWallet = wallet; + }, + }); + + await connect([Chain.Zcash]); + + return addedWallet; +} + +describe("noirWallet", () => { + let requests = []; + + beforeEach(() => { + requests = mockNoirWallet(); + }); + + test("exposes Zcash as the only supported chain", () => { + expect(noirWallet.connectNoirWallet.supportedChains).toEqual([Chain.Zcash]); + }); + + test("connects via zcash_requestAccounts and registers the transparent address", async () => { + const wallet = await connectAndGetWallet(); + + expect(requests.some(({ method }) => method === "zcash_requestAccounts")).toBe(true); + expect(wallet.address).toBe(TRANSPARENT_ADDRESS); + expect(wallet.chain).toBe(Chain.Zcash); + expect(wallet.walletType).toBe(WalletOption.NOIR_WALLET); + }); + + test("getBalance reads the wallet's spendable (shielded pool) balance", async () => { + const wallet = await connectAndGetWallet(); + const [balance] = await wallet.getBalance(); + + expect(balance.chain).toBe(Chain.Zcash); + expect(balance.getValue("string")).toBe("1.42"); + }); + + test("transfer delegates to zcash_sendTransaction without memo/type fields", async () => { + const wallet = await connectAndGetWallet(); + const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); + + const txid = await wallet.transfer({ assetValue, recipient: "t1RecipientAddr" }); + + expect(txid).toBe("mocktxid123"); + const sendCall = requests.find(({ method }) => method === "zcash_sendTransaction"); + expect(sendCall.params).toEqual([{ amount: "0.5", to: "t1RecipientAddr" }]); + }); + + test("transfer with memo throws (OP_RETURN routes unsupported)", async () => { + const wallet = await connectAndGetWallet(); + const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); + + expect(() => wallet.transfer({ assetValue, memo: "=:BTC.BTC:bc1q...", recipient: "t1RecipientAddr" })).toThrow( + "wallet_noir_wallet_memo_not_supported", + ); + }); + + test("signMessage returns the signature string", async () => { + const wallet = await connectAndGetWallet(); + + expect(await wallet.signMessage("hello swapkit")).toBe("sigabc"); + }); + + test("throws when the extension is not installed", () => { + globalThis.window = {}; + + const connect = noirWallet.connectNoirWallet.connectWallet({ addChain: () => undefined }); + + expect(connect([Chain.Zcash])).rejects.toThrow("wallet_noir_wallet_not_found"); + }); +}); diff --git a/packages/wallet-extensions/src/noir-wallet/index.ts b/packages/wallet-extensions/src/noir-wallet/index.ts new file mode 100644 index 000000000..78ef7c3be --- /dev/null +++ b/packages/wallet-extensions/src/noir-wallet/index.ts @@ -0,0 +1,81 @@ +import { AssetValue, Chain, type GenericTransferParams, SwapKitError, WalletOption } from "@swapkit/helpers"; +import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; +import type { NoirWalletZcashProvider } from "../types"; + +export function getNoirWalletZcashProvider(): NoirWalletZcashProvider { + const provider = window.noirwallet?.zcash; + + if (!provider) { + throw new SwapKitError("wallet_noir_wallet_not_found"); + } + + return provider; +} + +export const noirWallet = createWallet({ + connect: ({ addChain, walletType }) => + async function connectNoirWallet(_chains?: Chain[]) { + const provider = getNoirWalletZcashProvider(); + + // Opens the extension's connect-approval popup and returns the primary + // account's addresses. + const account = await provider.request({ method: "zcash_requestAccounts" }); + + if (!account?.transparent) { + throw new SwapKitError("wallet_noir_wallet_connection_failed"); + } + + const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + const toolbox = await getUtxoToolbox(Chain.Zcash); + + addChain({ + ...toolbox, + address: account.transparent, + chain: Chain.Zcash, + /** + * Noir Wallet spends from the shielded pool, so the spendable balance + * comes from the wallet itself rather than the transparent address' + * UTXO set. + */ + getBalance: async () => { + const balance = await provider.request({ method: "zcash_getBalance" }); + const value = balance?.available ?? balance?.spendable ?? balance?.total ?? "0"; + + return [AssetValue.from({ chain: Chain.Zcash, value })]; + }, + signMessage: async (message: string) => { + const { signature } = await provider.request({ method: "zcash_signMessage", params: [message, {}] }); + + return signature; + }, + /** + * Transaction building, fee selection and signing happen inside the + * extension. Memos are only supported towards shielded recipients, + * which rules out OP_RETURN-based routes (e.g. Maya/THORChain) — + * deposit-address routes such as NEAR Intents work. + */ + transfer: ({ recipient, assetValue, memo }: GenericTransferParams) => { + if (memo) { + throw new SwapKitError("wallet_noir_wallet_memo_not_supported"); + } + + if (!(recipient && assetValue)) { + throw new SwapKitError("wallet_missing_params", { params: { assetValue, recipient } }); + } + + return provider.request({ + method: "zcash_sendTransaction", + params: [{ amount: assetValue.getValue("string"), to: recipient }], + }); + }, + walletType, + }); + + return true; + }, + name: "connectNoirWallet", + supportedChains: [Chain.Zcash], + walletType: WalletOption.NOIR_WALLET, +}); + +export const NOIR_WALLET_SUPPORTED_CHAINS = getWalletSupportedChains(noirWallet); diff --git a/packages/wallet-extensions/src/types.ts b/packages/wallet-extensions/src/types.ts index 67f9ec2fa..8ab941dc3 100644 --- a/packages/wallet-extensions/src/types.ts +++ b/packages/wallet-extensions/src/types.ts @@ -21,6 +21,13 @@ export type VultisigCosmosProvider = { request(request: { method: string; params?: any[] | Record }, callback?: Callback): Promise; }; +export type NoirWalletZcashProvider = { + request(request: { method: string; params?: any[] }): Promise; + on(event: string, handler: (...args: any[]) => void): void; + removeListener?(event: string, handler: (...args: any[]) => void): void; + disconnect(): Promise; +}; + declare global { interface Window { injectedWeb3?: SubstrateInjectedExtension; @@ -48,6 +55,8 @@ declare global { near: NearBrowserWalletProvider; }; + noirwallet?: { isNoirWallet: boolean; version?: string; zcash: NoirWalletZcashProvider }; + vultisig?: { bitcoin: Eip1193Provider; bitcoincash: Eip1193Provider; diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 0f1842692..fdf7bb8fd 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -130,6 +130,12 @@ "require": "./dist/src/near-wallet-selector/index.cjs", "types": "./dist/types/near-wallet-selector/index.d.ts" }, + "./noir-wallet": { + "bun": "./src/noir-wallet.ts", + "default": "./dist/src/noir-wallet.js", + "require": "./dist/src/noir-wallet.cjs", + "types": "./dist/types/noir-wallet.d.ts" + }, "./okx": { "bun": "./src/okx.ts", "default": "./dist/src/okx.js", diff --git a/packages/wallets/src/noir-wallet.ts b/packages/wallets/src/noir-wallet.ts new file mode 100644 index 000000000..8c94cfb90 --- /dev/null +++ b/packages/wallets/src/noir-wallet.ts @@ -0,0 +1 @@ +export * from "@swapkit/wallet-extensions/noir-wallet"; diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index 6152f383c..df825db15 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -5,6 +5,7 @@ import type { ctrlWallet } from "@swapkit/wallet-extensions/ctrl"; import type { evmWallet } from "@swapkit/wallet-extensions/evm-extensions"; import type { keepkeyBexWallet } from "@swapkit/wallet-extensions/keepkey-bex"; import type { keplrWallet } from "@swapkit/wallet-extensions/keplr"; +import type { noirWallet } from "@swapkit/wallet-extensions/noir-wallet"; import type { okxWallet } from "@swapkit/wallet-extensions/okx"; import type { onekeyWallet } from "@swapkit/wallet-extensions/onekey"; import type { phantomWallet } from "@swapkit/wallet-extensions/phantom"; @@ -40,6 +41,7 @@ export type SKWallets = { [WalletOption.LEDGER]: typeof ledgerWallet; [WalletOption.LEDGER_LIVE]: typeof ledgerWallet; [WalletOption.METAMASK]: typeof evmWallet; + [WalletOption.NOIR_WALLET]: typeof noirWallet; [WalletOption.OKX]: typeof okxWallet; [WalletOption.OKX_MOBILE]: typeof evmWallet; [WalletOption.ONEKEY]: typeof onekeyWallet; diff --git a/packages/wallets/src/utils.ts b/packages/wallets/src/utils.ts index 84bf7cec9..de4c696d4 100644 --- a/packages/wallets/src/utils.ts +++ b/packages/wallets/src/utils.ts @@ -10,6 +10,7 @@ export async function loadWallet(walletOption: W): Promi .with(WalletOption.CTRL, async () => (await import("@swapkit/wallet-extensions/ctrl")).ctrlWallet) .with(WalletOption.VULTISIG, async () => (await import("@swapkit/wallet-extensions/vultisig")).vultisigWallet) .with(WalletOption.OKX, async () => (await import("@swapkit/wallet-extensions/okx")).okxWallet) + .with(WalletOption.NOIR_WALLET, async () => (await import("@swapkit/wallet-extensions/noir-wallet")).noirWallet) .with(WalletOption.ONEKEY, async () => (await import("@swapkit/wallet-extensions/onekey")).onekeyWallet) .with(WalletOption.EXODUS, async () => (await import("./passkeys")).passkeysWallet) .with(WalletOption.KEEPKEY, async () => (await import("@swapkit/wallet-hardware/keepkey")).keepkeyWallet) diff --git a/playgrounds/vite/src/WalletPicker.tsx b/playgrounds/vite/src/WalletPicker.tsx index 6338a9880..d7d267661 100644 --- a/playgrounds/vite/src/WalletPicker.tsx +++ b/playgrounds/vite/src/WalletPicker.tsx @@ -8,6 +8,7 @@ import { KEEPKEY_SUPPORTED_CHAINS } from "@swapkit/wallets/keepkey"; import { KEEPKEY_BEX_SUPPORTED_CHAINS } from "@swapkit/wallets/keepkey-bex"; import { KEPLR_SUPPORTED_CHAINS } from "@swapkit/wallets/keplr"; import { decryptFromKeystore, KEYSTORE_SUPPORTED_CHAINS } from "@swapkit/wallets/keystore"; +import { NOIR_WALLET_SUPPORTED_CHAINS } from "@swapkit/wallets/noir-wallet"; import { OKX_SUPPORTED_CHAINS } from "@swapkit/wallets/okx"; import { ONEKEY_WALLET_SUPPORTED_CHAINS } from "@swapkit/wallets/onekey"; import { PASSKEYS_SUPPORTED_CHAINS } from "@swapkit/wallets/passkeys"; @@ -51,6 +52,7 @@ export const availableChainsByWallet = { [WalletOption.LEAP]: KEPLR_SUPPORTED_CHAINS, [WalletOption.LEDGER]: LEDGER_SUPPORTED_CHAINS, [WalletOption.METAMASK]: EVMChains, + [WalletOption.NOIR_WALLET]: NOIR_WALLET_SUPPORTED_CHAINS, [WalletOption.OKX]: OKX_SUPPORTED_CHAINS, [WalletOption.OKX_MOBILE]: EVMChains, [WalletOption.ONEKEY]: ONEKEY_WALLET_SUPPORTED_CHAINS, @@ -134,6 +136,8 @@ export const WalletPicker = ({ skClient, setWallet, setPhrase }: Props) => { return skClient.connectVultisig?.(chainsToConnect); case WalletOption.OKX: return skClient.connectOkx?.(chainsToConnect); + case WalletOption.NOIR_WALLET: + return skClient.connectNoirWallet?.(chainsToConnect); case WalletOption.POLKADOT_JS: return skClient.connectPolkadotJs?.(chainsToConnect as Chain.Polkadot[]); From 164ca932a0c61e728a3c33205117b656e6d6f892 Mon Sep 17 00:00:00 2001 From: harry <691742@gmail.com> Date: Wed, 15 Jul 2026 10:49:41 +0800 Subject: [PATCH 2/3] fix(wallets): add NOIR_WALLET to SKWalletsSupportedChains type --- packages/wallets/src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index df825db15..80b4b7974 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -78,6 +78,7 @@ export type SKWalletsSupportedChains = { [WalletOption.LEDGER]: typeof ledgerWallet.connectLedger.supportedChains; [WalletOption.LEDGER_LIVE]: typeof ledgerWallet.connectLedger.supportedChains; [WalletOption.METAMASK]: typeof evmWallet.connectEVMWallet.supportedChains; + [WalletOption.NOIR_WALLET]: typeof noirWallet.connectNoirWallet.supportedChains; [WalletOption.OKX]: typeof okxWallet.connectOkx.supportedChains; [WalletOption.OKX_MOBILE]: typeof evmWallet.connectEVMWallet.supportedChains; [WalletOption.ONEKEY]: typeof onekeyWallet.connectOnekeyWallet.supportedChains; From cae972f14310ed221f16bd214b85efdd5332e782 Mon Sep 17 00:00:00 2001 From: harry <691742@gmail.com> Date: Wed, 15 Jul 2026 15:45:48 +0800 Subject: [PATCH 3/3] fix(wallets): pass memo through to Noir Wallet extension The extension now supports memos for shielded recipients (zs/u1), so remove the blanket memo rejection from the connector and let the wallet decide validity. Memos to transparent addresses (t1/t3) are still rejected at the extension level. --- packages/helpers/src/modules/swapKitError.ts | 1 - .../src/noir-wallet/__tests__/noir-wallet.test.ts | 12 +++++++----- packages/wallet-extensions/src/noir-wallet/index.ts | 13 +++++-------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/helpers/src/modules/swapKitError.ts b/packages/helpers/src/modules/swapKitError.ts index d253ea80d..a46eb778f 100644 --- a/packages/helpers/src/modules/swapKitError.ts +++ b/packages/helpers/src/modules/swapKitError.ts @@ -251,7 +251,6 @@ const errorCodes = { * Wallets - Noir Wallet */ wallet_noir_wallet_not_found: 22201, - wallet_noir_wallet_memo_not_supported: 22202, wallet_noir_wallet_connection_failed: 22203, /** diff --git a/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts b/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts index d11f55e32..1c71710b6 100644 --- a/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts +++ b/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts @@ -91,7 +91,7 @@ describe("noirWallet", () => { expect(balance.getValue("string")).toBe("1.42"); }); - test("transfer delegates to zcash_sendTransaction without memo/type fields", async () => { + test("transfer without memo omits memo field", async () => { const wallet = await connectAndGetWallet(); const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); @@ -102,13 +102,15 @@ describe("noirWallet", () => { expect(sendCall.params).toEqual([{ amount: "0.5", to: "t1RecipientAddr" }]); }); - test("transfer with memo throws (OP_RETURN routes unsupported)", async () => { + test("transfer with memo passes it to the extension", async () => { const wallet = await connectAndGetWallet(); const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); - expect(() => wallet.transfer({ assetValue, memo: "=:BTC.BTC:bc1q...", recipient: "t1RecipientAddr" })).toThrow( - "wallet_noir_wallet_memo_not_supported", - ); + const txid = await wallet.transfer({ assetValue, memo: "test memo", recipient: "zs1shieldedAddr" }); + + expect(txid).toBe("mocktxid123"); + const sendCall = requests.find(({ method }) => method === "zcash_sendTransaction"); + expect(sendCall.params).toEqual([{ amount: "0.5", memo: "test memo", to: "zs1shieldedAddr" }]); }); test("signMessage returns the signature string", async () => { diff --git a/packages/wallet-extensions/src/noir-wallet/index.ts b/packages/wallet-extensions/src/noir-wallet/index.ts index 78ef7c3be..ac978d7aa 100644 --- a/packages/wallet-extensions/src/noir-wallet/index.ts +++ b/packages/wallet-extensions/src/noir-wallet/index.ts @@ -50,22 +50,19 @@ export const noirWallet = createWallet({ }, /** * Transaction building, fee selection and signing happen inside the - * extension. Memos are only supported towards shielded recipients, - * which rules out OP_RETURN-based routes (e.g. Maya/THORChain) — - * deposit-address routes such as NEAR Intents work. + * extension. Memos are supported for shielded recipients (zs/u1); + * the extension rejects memos to transparent addresses (t1/t3), + * which means OP_RETURN-based routes (Maya/THORChain) will fail at + * the wallet level while deposit-address routes (NEAR Intents) work. */ transfer: ({ recipient, assetValue, memo }: GenericTransferParams) => { - if (memo) { - throw new SwapKitError("wallet_noir_wallet_memo_not_supported"); - } - if (!(recipient && assetValue)) { throw new SwapKitError("wallet_missing_params", { params: { assetValue, recipient } }); } return provider.request({ method: "zcash_sendTransaction", - params: [{ amount: assetValue.getValue("string"), to: recipient }], + params: [{ amount: assetValue.getValue("string"), to: recipient, ...(memo && { memo }) }], }); }, walletType,