diff --git a/modules/sdk-core/src/bitgo/enterprise/enterprise.ts b/modules/sdk-core/src/bitgo/enterprise/enterprise.ts index 0a2a8ced4f..da4f7e6124 100644 --- a/modules/sdk-core/src/bitgo/enterprise/enterprise.ts +++ b/modules/sdk-core/src/bitgo/enterprise/enterprise.ts @@ -7,6 +7,7 @@ import { BitGoBase } from '../bitgoBase'; import { EnterpriseData, EnterpriseFeatureFlag, IEnterprise } from '../enterprise'; import { getFirstPendingTransaction } from '../internal'; import { ListWalletOptions, Wallet } from '../wallet'; +import { Safes } from '../safe'; import { BitGoProofSignatures, EcdsaUtils, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa'; import { EcdsaTypes } from '@bitgo/sdk-lib-mpc'; import { verifyEcdhSignature } from '../ecdh'; @@ -249,4 +250,12 @@ export class Enterprise implements IEnterprise { hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean { return flags.every((targetFlag) => this._enterprise.featureFlags?.includes(targetFlag)); } + + /** + * Get the safes collection accessor scoped to this Enterprise + * @experimental + */ + safes(): Safes { + return new Safes(this.bitgo, this.id); + } } diff --git a/modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts b/modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts index 60e96f2df9..e2727916a9 100644 --- a/modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts +++ b/modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts @@ -3,6 +3,7 @@ import { IWallet } from '../wallet'; import { Buffer } from 'buffer'; import { BitGoProofSignatures, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa'; import { EcdhDerivedKeypair } from '../keychain'; +import { ISafes } from '../safe'; // useEnterpriseEcdsaTssChallenge is deprecated export type EnterpriseFeatureFlag = 'useEnterpriseEcdsaTssChallenge'; @@ -39,4 +40,6 @@ export interface IEnterprise { bitgoNitroChallenge: SerializedNtildeWithVerifiers ): Promise; hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean; + /** @experimental */ + safes(): ISafes; } diff --git a/modules/sdk-core/src/bitgo/index.ts b/modules/sdk-core/src/bitgo/index.ts index 610c75d97d..18b116ce4c 100644 --- a/modules/sdk-core/src/bitgo/index.ts +++ b/modules/sdk-core/src/bitgo/index.ts @@ -27,6 +27,7 @@ export * from './tss'; export { sendSignatureShare } from './tss'; export * from './types'; export * from './utils'; +export * from './safe'; export * from './wallet'; export * from './webhook'; export { bitcoinUtil }; diff --git a/modules/sdk-core/src/bitgo/safe/iSafe.ts b/modules/sdk-core/src/bitgo/safe/iSafe.ts new file mode 100644 index 0000000000..9c587da711 --- /dev/null +++ b/modules/sdk-core/src/bitgo/safe/iSafe.ts @@ -0,0 +1,97 @@ +/** + * @prettier + * + * @experimental The safe client surface is experimental and may change (including breaking + * changes) before the public release. + */ +// Wire/data shapes are owned by @bitgo/public-types — import them where needed rather than +// re-exporting from sdk-core. +import type { + FreezeSafeBody, + SafeData, + SafePermission, + SafeRootKeys, + SafeShareData, + SafeShareKeychain, + SafeShareState, +} from '@bitgo/public-types'; +import type { Wallet, WalletShare } from '../wallet'; + +export interface InitializeSafeOptions { + label: string; +} + +// Phase 3 — the client hands back the 12 key ids it created in Phase 2: +export interface FinalizeSafeOptions { + rootKeys: SafeRootKeys; +} + +/** + * Sharing ONE safe wallet with a non-member rides the existing wallet-share handshake (FR-13), + * so the result is the existing WalletShare shape. + */ +export type WalletShareData = WalletShare; + +// ---- per-safe operation options (bodies land in WCN-1203 / WCN-1204) ---- + +export interface CreateSafeWalletOptions { + coin: string; + label: string; + type?: string; + multisigTypeVersion?: string; +} + +interface AddSafeMemberBase { + permissions: SafePermission[]; + /** required when 'spend' is included — the 4 root user keys ECDH-re-encrypted to the invitee */ + keychains?: SafeShareKeychain[]; + message?: string; + /** when true, suppress the invitation email that would otherwise be sent to `email` */ + disableEmail?: boolean; +} + +/** Add a member by either `userId` or `email` — exactly one is required. */ +export type AddSafeMemberOptions = + | (AddSafeMemberBase & { userId: string; email?: never }) + | (AddSafeMemberBase & { email: string; userId?: never }); + +export interface AddSafeWalletMemberOptions { + walletId: string; + /** required — sharing re-encrypts the user key, which needs hardened derivation from the passphrase */ + walletPassphrase: string; + email?: string; + permissions?: string[]; + message?: string; +} + +export type AcceptSafeShareAsSpenderOptions = { + safeShareId: string; + userPassword: string; + newWalletPassphrase?: string; +}; +export type AcceptSafeShareAsNonSpenderOptions = { + safeShareId: string; +}; +export type AcceptSafeShareOptions = AcceptSafeShareAsSpenderOptions | AcceptSafeShareAsNonSpenderOptions; + +/** + * @experimental + */ +export interface ISafe { + id(): string; + enterpriseId(): string; + label(): string; + status(): SafeData['status']; + url(extra?: string): string; + createWallet(params: CreateSafeWalletOptions): Promise; + // whole-safe: view/admin/spend/dapp; spend opens a key share (also how a spender services a + // safeShareRequests entry in UMS orgs) + addMember(params: AddSafeMemberOptions): Promise; + // share ONE safe wallet, not the whole safe + addMemberToWallet(params: AddSafeWalletMemberOptions): Promise; + listShares(params?: { state?: SafeShareState }): Promise; + acceptShare(params: AcceptSafeShareOptions): Promise; + freeze(params?: FreezeSafeBody): Promise; + archive(): Promise; + toJSON(): SafeData; +} diff --git a/modules/sdk-core/src/bitgo/safe/iSafes.ts b/modules/sdk-core/src/bitgo/safe/iSafes.ts new file mode 100644 index 0000000000..467d033f73 --- /dev/null +++ b/modules/sdk-core/src/bitgo/safe/iSafes.ts @@ -0,0 +1,60 @@ +/** + * @prettier + * + * @experimental The safe client surface is experimental and may change (including breaking + * changes) before the public release. + */ +import { FinalizeSafeOptions, InitializeSafeOptions } from './iSafe'; +import { Safe } from './safe'; + +/** + * Options for safe creation. + * + * v1 targets the HOT custody model only: the multisig root user/backup keys are generated locally + * by the SDK, encrypted with `passphrase`, and registered on BitGo; the MPC roots run the standard + * hot ceremonies with the same passphrase. Self-managed cold keys and custodial safes are out of + * scope for v1. + */ +export interface CreateSafeOptions { + label: string; + passphrase: string; // encrypts the locally-generated multisig user/backup prvs; shared with the MPC ceremonies +} + +/** Handle returned by `initializeSafe`, threaded into the key ceremonies and finalize. */ +export interface SafeCreationHandle { + safeId: string; +} + +/** + * The 12 minted root key ids produced by `createSafeKeys`, as 4 ordered [user, backup, bitgo] + * triplets — exactly the payload `finalizeSafe` consumes. + */ +export type SafeKeys = FinalizeSafeOptions; + +export interface ListSafesOptions { + cursor?: string; // opaque cursor from a previous response's nextCursor + limit?: number; +} + +export interface GetSafeOptions { + id: string; +} + +/** + * @experimental + */ +export interface ISafes { + /** + * One-call convenience wrapper: initialize → createSafeKeys (4 safeId-tagged ceremonies) → + * finalize → keycard. HOT custody only in v1. + */ + generateSafe(params: CreateSafeOptions): Promise; + /** Phase 1 — initialize a safe (metadata only, no key material). */ + initializeSafe(params: InitializeSafeOptions): Promise; + /** Phase 2 — run the 4 root key ceremonies tagged with `safeId`; returns the 12 minted key ids. */ + createSafeKeys(params: CreateSafeOptions & SafeCreationHandle): Promise; + /** Phase 3 — finalize a safe with the 12 root key ids. Idempotent. */ + finalizeSafe(safeId: string, params: FinalizeSafeOptions): Promise; + list(params?: ListSafesOptions): Promise<{ safes: Safe[]; nextCursor?: string }>; + get(params: GetSafeOptions): Promise; +} diff --git a/modules/sdk-core/src/bitgo/safe/index.ts b/modules/sdk-core/src/bitgo/safe/index.ts new file mode 100644 index 0000000000..5a4f621ee3 --- /dev/null +++ b/modules/sdk-core/src/bitgo/safe/index.ts @@ -0,0 +1,4 @@ +export * from './iSafe'; +export * from './iSafes'; +export * from './safe'; +export * from './safes'; diff --git a/modules/sdk-core/src/bitgo/safe/safe.ts b/modules/sdk-core/src/bitgo/safe/safe.ts new file mode 100644 index 0000000000..eee1843e0a --- /dev/null +++ b/modules/sdk-core/src/bitgo/safe/safe.ts @@ -0,0 +1,128 @@ +/** + * @prettier + * + * @experimental The safe client surface is experimental and may change (including breaking + * changes) before the public release. + */ +import * as _ from 'lodash'; +import { FreezeSafeBody, SafeData, SafeShareData, SafeShareState } from '@bitgo/public-types'; +import { BitGoBase } from '../bitgoBase'; +import { decodeWithCodec } from '../utils/codecs'; +import { postWithCodec } from '../utils/postWithCodec'; +import { Wallet } from '../wallet'; +import { + AcceptSafeShareOptions, + AddSafeMemberOptions, + AddSafeWalletMemberOptions, + CreateSafeWalletOptions, + ISafe, + WalletShareData, +} from './iSafe'; + +/** + * @experimental + */ +export class Safe implements ISafe { + private readonly bitgo: BitGoBase; + public readonly _safe: SafeData; + + constructor(bitgo: BitGoBase, safeData: SafeData) { + this.bitgo = bitgo; + if (!_.isObject(safeData)) { + throw new Error('safeData has to be an object'); + } + if (!_.isString(safeData.id)) { + throw new Error('safe id has to be a string'); + } + if (!_.isString(safeData.enterpriseId)) { + throw new Error('safe enterpriseId has to be a string'); + } + this._safe = safeData; + } + + id(): string { + return this._safe.id; + } + + enterpriseId(): string { + return this._safe.enterpriseId; + } + + label(): string { + return this._safe.label; + } + + status(): SafeData['status'] { + return this._safe.status; + } + + /** + * Enterprise-scoped v2 URL for this safe, e.g. /api/v2/enterprise/:eId/safes/:safeId + * @param extra + */ + url(extra = ''): string { + return this.bitgo.url(`/enterprise/${this.enterpriseId()}/safes/${this.id()}${extra}`, 2); + } + + /** + * Mint a child wallet in this safe (server-side public derivation — no ceremony). + * Body lands in WCN-1203. + */ + async createWallet(params: CreateSafeWalletOptions): Promise { + throw new Error('Safe.createWallet is not yet implemented (WCN-1203)'); + } + + /** + * Add a member to the whole safe (view/admin/spend). Spend opens a key share. + * Body lands in WCN-1204. + */ + async addMember(params: AddSafeMemberOptions): Promise { + throw new Error('Safe.addMember is not yet implemented (WCN-1204)'); + } + + /** + * Share ONE safe wallet with a non-member via the existing wallet-share handshake (FR-13). + * Body lands in WCN-1204. + */ + async addMemberToWallet(params: AddSafeWalletMemberOptions): Promise { + throw new Error('Safe.addMemberToWallet is not yet implemented (WCN-1204)'); + } + + /** + * List the safe key shares visible to the caller. + * Body lands in WCN-1204. + */ + async listShares(params: { state?: SafeShareState } = {}): Promise { + throw new Error('Safe.listShares is not yet implemented (WCN-1204)'); + } + + /** + * Accept a safe key share addressed to the caller. + * Body lands in WCN-1204. + */ + async acceptShare(params: AcceptSafeShareOptions): Promise { + throw new Error('Safe.acceptShare is not yet implemented (WCN-1204)'); + } + + /** + * Freeze the safe — blocks withdrawals on all safe wallets. Safe stays 'active'. + * @param params + */ + async freeze(params: FreezeSafeBody = {}): Promise { + const response = await postWithCodec(this.bitgo, this.url('/freeze'), FreezeSafeBody, params).result(); + return decodeWithCodec(SafeData, response, 'SafeData'); + } + + /** + * Archive the safe. Requires every safe wallet to already be archived; also the abandonment + * path for a stuck 'initializing' safe. + */ + async archive(): Promise { + const response = await this.bitgo.post(this.url('/archive')).send().result(); + return decodeWithCodec(SafeData, response, 'SafeData'); + } + + toJSON(): SafeData { + return this._safe; + } +} diff --git a/modules/sdk-core/src/bitgo/safe/safes.ts b/modules/sdk-core/src/bitgo/safe/safes.ts new file mode 100644 index 0000000000..230359bd94 --- /dev/null +++ b/modules/sdk-core/src/bitgo/safe/safes.ts @@ -0,0 +1,83 @@ +/** + * @prettier + * + * @experimental The safe client surface is experimental and may change (including breaking + * changes) before the public release. + */ +import { BitGoBase } from '../bitgoBase'; +import { FinalizeSafeOptions, InitializeSafeOptions } from './iSafe'; +import { CreateSafeOptions, GetSafeOptions, ISafes, ListSafesOptions, SafeCreationHandle, SafeKeys } from './iSafes'; +import { Safe } from './safe'; + +/** + * Collection accessor for a single enterprise's safes, mirroring Wallets / Enterprises. + * Safe routes are enterprise-scoped (/api/v2/enterprise/:eId/safes), so the accessor is + * constructed with the enterprise id (see Enterprise.safes()). + * + * @experimental + */ +export class Safes implements ISafes { + private readonly bitgo: BitGoBase; + private readonly enterpriseId: string; + + constructor(bitgo: BitGoBase, enterpriseId: string) { + this.bitgo = bitgo; + this.enterpriseId = enterpriseId; + } + + /** + * Enterprise-scoped v2 URL for the safes collection, e.g. /api/v2/enterprise/:eId/safes + * @param extra + */ + private url(extra = ''): string { + return this.bitgo.url(`/enterprise/${this.enterpriseId}/safes${extra}`, 2); + } + + /** + * One-call convenience wrapper: initialize → createSafeKeys (4 safeId-tagged ceremonies) → + * finalize → keycard. HOT custody only. Implemented in WCN-1192 Phase 2. + */ + async generateSafe(params: CreateSafeOptions): Promise { + throw new Error('Safes.generateSafe is not yet implemented (WCN-1192 Phase 2)'); + } + + /** + * Phase 1 — initialize a safe (metadata only, no key material). + * Implemented in WCN-1192 Phase 2 (blocked on WCN-1175). + */ + async initializeSafe(params: InitializeSafeOptions): Promise { + throw new Error('Safes.initializeSafe is not yet implemented (WCN-1192 Phase 2)'); + } + + /** + * Phase 2 — run the 4 root key ceremonies tagged with `safeId`; returns the 12 minted key ids. + * Implemented in WCN-1192 Phase 2 (blocked on WCN-1176). + */ + async createSafeKeys(params: CreateSafeOptions & SafeCreationHandle): Promise { + throw new Error('Safes.createSafeKeys is not yet implemented (WCN-1192 Phase 2)'); + } + + /** + * Phase 3 — finalize a safe with the 12 root key ids as 4 ordered [user, backup, bitgo] triplets. + * Idempotent. Implemented in WCN-1192 Phase 2 (blocked on WCN-1175). + */ + async finalizeSafe(safeId: string, params: FinalizeSafeOptions): Promise { + throw new Error('Safes.finalizeSafe is not yet implemented (WCN-1192 Phase 2)'); + } + + /** + * List the enterprise's safes (cursor pagination). + * Implemented in WCN-1192 Phase 3 (blocked on WCN-1177). + */ + async list(params: ListSafesOptions = {}): Promise<{ safes: Safe[]; nextCursor?: string }> { + throw new Error('Safes.list is not yet implemented (WCN-1192 Phase 3)'); + } + + /** + * Fetch a single safe by id. + * Implemented in WCN-1192 Phase 3 (blocked on WCN-1177). + */ + async get(params: GetSafeOptions): Promise { + throw new Error('Safes.get is not yet implemented (WCN-1192 Phase 3)'); + } +} diff --git a/modules/sdk-core/test/unit/bitgo/safe/safe.ts b/modules/sdk-core/test/unit/bitgo/safe/safe.ts new file mode 100644 index 0000000000..0f6ff3bf86 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/safe/safe.ts @@ -0,0 +1,133 @@ +import * as sinon from 'sinon'; +import 'should'; +import { SafeData } from '@bitgo/public-types'; +import { Safe } from '../../../../src'; + +describe('Safe', function () { + let safe: Safe; + let mockBitGo: any; + let safeData: SafeData; + // wire-shaped SafeData (timestamps as ISO strings) for mocked REST responses that get decoded + let safeDataWire: any; + + beforeEach(function () { + mockBitGo = { + url: sinon.stub().callsFake((path: string) => path), + post: sinon.stub(), + }; + safeData = { + id: 'test-safe-id', + enterpriseId: 'test-enterprise-id', + label: 'my safe', + status: 'active', + creator: 'creator-id', + users: [{ userId: 'creator-id', permissions: ['admin', 'spend'] }], + createdAt: new Date('2026-07-07T00:00:00.000Z'), + }; + safeDataWire = { + id: 'test-safe-id', + enterpriseId: 'test-enterprise-id', + label: 'my safe', + status: 'active', + creator: 'creator-id', + users: [{ userId: 'creator-id', permissions: ['admin', 'spend'] }], + createdAt: '2026-07-07T00:00:00.000Z', + }; + safe = new Safe(mockBitGo, safeData); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('constructor', function () { + it('throws when safeData is not an object', function () { + (() => new Safe(mockBitGo, undefined as unknown as SafeData)).should.throw('safeData has to be an object'); + }); + + it('throws when id is not a string', function () { + (() => new Safe(mockBitGo, { ...safeData, id: 123 as unknown as string })).should.throw( + 'safe id has to be a string' + ); + }); + + it('throws when enterpriseId is not a string', function () { + (() => + new Safe(mockBitGo, { + ...safeData, + enterpriseId: undefined as unknown as string, + })).should.throw('safe enterpriseId has to be a string'); + }); + }); + + describe('getters', function () { + it('exposes id/enterpriseId/label/status', function () { + safe.id().should.equal('test-safe-id'); + safe.enterpriseId().should.equal('test-enterprise-id'); + safe.label().should.equal('my safe'); + safe.status().should.equal('active'); + }); + + it('toJSON returns the underlying data', function () { + safe.toJSON().should.equal(safeData); + }); + }); + + describe('url', function () { + it('builds the enterprise-scoped v2 path', function () { + safe.url().should.equal('/enterprise/test-enterprise-id/safes/test-safe-id'); + safe.url('/freeze').should.equal('/enterprise/test-enterprise-id/safes/test-safe-id/freeze'); + mockBitGo.url.calledWith('/enterprise/test-enterprise-id/safes/test-safe-id', 2).should.be.true(); + }); + }); + + describe('freeze / archive REST plumbing', function () { + function stubPost(response: unknown) { + const resultStub = sinon.stub().resolves(response); + const sendStub = sinon.stub().returns({ result: resultStub }); + mockBitGo.post.returns({ send: sendStub }); + return { sendStub }; + } + + it('freeze posts the encoded body to the freeze route and decodes SafeData', async function () { + const frozen = { ...safeDataWire, freeze: { reason: 'incident' } }; + const { sendStub } = stubPost(frozen); + const result = await safe.freeze({ reason: 'incident' }); + mockBitGo.post.calledWith('/enterprise/test-enterprise-id/safes/test-safe-id/freeze').should.be.true(); + sendStub.calledWith({ reason: 'incident' }).should.be.true(); + result.freeze!.reason!.should.equal('incident'); + result.createdAt.should.be.instanceof(Date); + }); + + it('archive posts an empty body to the archive route and decodes SafeData', async function () { + const archived = { ...safeDataWire, status: 'archived' as const }; + const { sendStub } = stubPost(archived); + const result = await safe.archive(); + mockBitGo.post.calledWith('/enterprise/test-enterprise-id/safes/test-safe-id/archive').should.be.true(); + sendStub.calledWithExactly().should.be.true(); + result.status.should.equal('archived'); + }); + }); + + describe('member/share methods are stubbed (WCN-1203 / WCN-1204)', function () { + it('createWallet throws not-implemented (WCN-1203)', async function () { + await safe.createWallet({ coin: 'tbtc', label: 'w' }).should.be.rejectedWith(/WCN-1203/); + }); + + it('addMember throws not-implemented (WCN-1204)', async function () { + await safe.addMember({ userId: 'u', permissions: ['view'] }).should.be.rejectedWith(/WCN-1204/); + }); + + it('addMemberToWallet throws not-implemented (WCN-1204)', async function () { + await safe.addMemberToWallet({ walletId: 'w', walletPassphrase: 'p' }).should.be.rejectedWith(/WCN-1204/); + }); + + it('listShares throws not-implemented (WCN-1204)', async function () { + await safe.listShares().should.be.rejectedWith(/WCN-1204/); + }); + + it('acceptShare throws not-implemented (WCN-1204)', async function () { + await safe.acceptShare({ safeShareId: 's' }).should.be.rejectedWith(/WCN-1204/); + }); + }); +}); diff --git a/modules/sdk-core/test/unit/bitgo/safe/safes.ts b/modules/sdk-core/test/unit/bitgo/safe/safes.ts new file mode 100644 index 0000000000..813f6fb149 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/safe/safes.ts @@ -0,0 +1,68 @@ +import * as sinon from 'sinon'; +import 'should'; +import { Enterprise, Safes } from '../../../../src'; + +describe('Safes', function () { + let safes: Safes; + let mockBitGo: any; + + beforeEach(function () { + mockBitGo = { + url: sinon.stub().callsFake((path: string) => path), + }; + safes = new Safes(mockBitGo, 'test-enterprise-id'); + }); + + afterEach(function () { + sinon.restore(); + }); + + // Phase 2 (generateSafe/initialize/createSafeKeys/finalize) and Phase 3 (list/get) are not yet + // implemented — they are gated on WCN-1175 / WCN-1176 / WCN-1177. Assert the interface is wired + // and stubbed for now. + describe('unimplemented lifecycle methods', function () { + it('generateSafe throws (Phase 2)', async function () { + await safes.generateSafe({ label: 'v', passphrase: 'p' }).should.be.rejectedWith(/not yet implemented .*Phase 2/); + }); + + it('initializeSafe throws (Phase 2)', async function () { + await safes.initializeSafe({ label: 'v' }).should.be.rejectedWith(/not yet implemented .*Phase 2/); + }); + + it('createSafeKeys throws (Phase 2)', async function () { + await safes + .createSafeKeys({ label: 'v', passphrase: 'p', safeId: 'vid' }) + .should.be.rejectedWith(/not yet implemented .*Phase 2/); + }); + + it('finalizeSafe throws (Phase 2)', async function () { + await safes + .finalizeSafe('vid', { + rootKeys: { + hot: { + secp256k1Multisig: ['u', 'b', 'g'], + ecdsaMpc: ['u', 'b', 'g'], + eddsaMpc: ['u', 'b', 'g'], + ed25519Multisig: ['u', 'b', 'g'], + }, + }, + }) + .should.be.rejectedWith(/not yet implemented .*Phase 2/); + }); + + it('list throws (Phase 3)', async function () { + await safes.list().should.be.rejectedWith(/not yet implemented .*Phase 3/); + }); + + it('get throws (Phase 3)', async function () { + await safes.get({ id: 'vid' }).should.be.rejectedWith(/not yet implemented .*Phase 3/); + }); + }); + + describe('Enterprise.safes() accessor', function () { + it('returns a Safes instance scoped to the enterprise', function () { + const enterprise = new Enterprise(mockBitGo, {} as any, { id: 'ent-id', name: 'ent' }); + enterprise.safes().should.be.instanceof(Safes); + }); + }); +});