Skip to content
43 changes: 41 additions & 2 deletions packages/services/guard/src/client/guard.gen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// sequence-guard v0.4.0 a465c693f2fdb46b87f61056f04c784acfcd4944
// sequence-guard v0.4.0 b62e755c3f81d6b5a8e7462abc063a57a744cdef
// --
// Code generated by webrpc-gen@v0.25.3 with typescript generator. DO NOT EDIT.
//
Expand All @@ -16,7 +16,7 @@ export const WebRPCVersion = 'v1'
export const WebRPCSchemaVersion = 'v0.4.0'

// Schema hash generated from your RIDL schema
export const WebRPCSchemaHash = 'a465c693f2fdb46b87f61056f04c784acfcd4944'
export const WebRPCSchemaHash = 'b62e755c3f81d6b5a8e7462abc063a57a744cdef'

type WebrpcGenVersions = {
webrpcGenVersion: string
Expand Down Expand Up @@ -125,6 +125,7 @@ export interface OwnershipProof {
timestamp: number
signer: string
signature: string
chainId: number
}

export interface AuthToken {
Expand Down Expand Up @@ -271,18 +272,21 @@ export interface SetPINArgs {
pin: string
timestamp: number
signature: string
chainId: number
}

export interface SetPINReturn {}
export interface ResetPINArgs {
timestamp: number
signature: string
chainId: number
}

export interface ResetPINReturn {}
export interface CreateTOTPArgs {
timestamp: number
signature: string
chainId: number
}

export interface CreateTOTPReturn {
Expand All @@ -298,6 +302,7 @@ export interface CommitTOTPReturn {
export interface ResetTOTPArgs {
timestamp: number
signature: string
chainId: number
}

export interface ResetTOTPReturn {}
Expand All @@ -310,6 +315,7 @@ export interface Reset2FAReturn {}
export interface RecoveryCodesArgs {
timestamp: number
signature: string
chainId: number
}

export interface RecoveryCodesReturn {
Expand All @@ -318,6 +324,7 @@ export interface RecoveryCodesReturn {
export interface ResetRecoveryCodesArgs {
timestamp: number
signature: string
chainId: number
}

export interface ResetRecoveryCodesReturn {
Expand Down Expand Up @@ -964,6 +971,32 @@ export class NotFoundError extends WebrpcError {
}
}

export class RequiresTOTPError extends WebrpcError {
constructor(
name: string = 'RequiresTOTP',
code: number = 6600,
message: string = `TOTP is required`,
status: number = 0,
cause?: string,
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, RequiresTOTPError.prototype)
}
}

export class RequiresPINError extends WebrpcError {
constructor(
name: string = 'RequiresPIN',
code: number = 6601,
message: string = `PIN is required`,
status: number = 0,
cause?: string,
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, RequiresPINError.prototype)
}
}

export enum errors {
WebrpcEndpoint = 'WebrpcEndpoint',
WebrpcRequestFailed = 'WebrpcRequestFailed',
Expand All @@ -989,6 +1022,8 @@ export enum errors {
QueryFailed = 'QueryFailed',
ValidationFailed = 'ValidationFailed',
NotFound = 'NotFound',
RequiresTOTP = 'RequiresTOTP',
RequiresPIN = 'RequiresPIN',
}

export enum WebrpcErrorCodes {
Expand Down Expand Up @@ -1016,6 +1051,8 @@ export enum WebrpcErrorCodes {
QueryFailed = 2003,
ValidationFailed = 2004,
NotFound = 3000,
RequiresTOTP = 6600,
RequiresPIN = 6601,
}

export const webrpcErrorByCode: { [code: number]: any } = {
Expand Down Expand Up @@ -1043,6 +1080,8 @@ export const webrpcErrorByCode: { [code: number]: any } = {
[2003]: QueryFailedError,
[2004]: ValidationFailedError,
[3000]: NotFoundError,
[6600]: RequiresTOTPError,
[6601]: RequiresPINError,
}

export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise<Response>
1 change: 1 addition & 0 deletions packages/services/guard/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './types.js'
export { PayloadType, SignatureType, type Signature } from './client/guard.gen.js'

export * as Client from './client/guard.gen.js'
export * as Sequence from './sequence.js'
export * as Local from './local.js'
8 changes: 8 additions & 0 deletions packages/services/guard/src/sequence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class Guard implements Types.Guard {
digest: Bytes.Bytes,
message: Bytes.Bytes,
signatures?: Client.Signature[],
token?: Client.AuthToken,
) {
if (!this.guard || !this.address) {
throw new Error('Guard not initialized')
Expand All @@ -36,11 +37,18 @@ export class Guard implements Types.Guard {
payloadData: Hex.fromBytes(message),
signatures,
},
token,
})

Hex.assert(res.sig)
return Signature.fromHex(res.sig)
} catch (error) {
if (error instanceof Client.RequiresTOTPError) {
throw new Types.AuthRequiredError('TOTP')
}
if (error instanceof Client.RequiresPINError) {
throw new Types.AuthRequiredError('PIN')
}
console.error(error)
throw new Error('Error signing with guard')
}
Expand Down
12 changes: 12 additions & 0 deletions packages/services/guard/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,17 @@ export interface Guard {
digest: Bytes.Bytes,
message: Bytes.Bytes,
signatures?: Client.Signature[],
token?: Client.AuthToken,
): Promise<Signature.Signature>
}

export class AuthRequiredError extends Error {
public readonly id: 'TOTP' | 'PIN'

constructor(id: 'TOTP' | 'PIN') {
super('auth required')
this.id = id
this.name = 'AuthRequiredError'
Object.setPrototypeOf(this, AuthRequiredError.prototype)
}
}
11 changes: 10 additions & 1 deletion packages/wallet/core/src/signers/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ import { Attestation, Payload } from '@0xsequence/wallet-primitives'
import * as GuardService from '@0xsequence/guard'
import * as Envelope from '../envelope.js'

type GuardToken = {
id: 'TOTP' | 'PIN'
code: string
}

export class Guard {
public readonly address: Address.Address

constructor(private readonly guard: GuardService.Guard) {
this.address = this.guard.address
}

async signEnvelope<T extends Payload.Payload>(envelope: Envelope.Signed<T>): Promise<Envelope.Signature> {
async signEnvelope<T extends Payload.Payload>(
envelope: Envelope.Signed<T>,
token?: GuardToken,
): Promise<Envelope.Signature> {
// Important: guard must always sign without parent wallets, even if the payload is parented
const unparentedPayload = {
...envelope.payload,
Expand All @@ -28,6 +36,7 @@ export class Guard {
digest,
message,
previousSignatures,
token ? { id: token.id, token: token.code } : undefined,
)
return {
address: this.guard.address,
Expand Down
16 changes: 12 additions & 4 deletions packages/wallet/core/test/signers-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,9 @@ describe('Guard Signer', () => {

const signatures = [mockHashSignature, mockEthSignSignature, mockErc1271Signature, mockSapientSignature]
const signedEnvelope = Envelope.toSigned(envelope, signatures)
const token = { id: 'TOTP' as const, code: '123456' }

const result = await guard.signEnvelope(signedEnvelope)
const result = await guard.signEnvelope(signedEnvelope, token)
expect(result).toEqual({
address: TEST_ADDRESS_1,
signature: {
Expand All @@ -137,6 +138,7 @@ describe('Guard Signer', () => {
expectedDigest,
expectedMessage,
expectedSignatures,
{ id: 'TOTP', token: '123456' },
)
})

Expand All @@ -159,8 +161,9 @@ describe('Guard Signer', () => {

const signatures = [mockHashSignature, mockEthSignSignature, mockErc1271Signature, mockSapientSignature]
const signedEnvelope = Envelope.toSigned(envelope, signatures)
const token = { id: 'TOTP' as const, code: '123456' }

const result = await guard.signEnvelope(signedEnvelope)
const result = await guard.signEnvelope(signedEnvelope, token)
expect(result).toEqual({
address: TEST_ADDRESS_1,
signature: {
Expand All @@ -182,6 +185,7 @@ describe('Guard Signer', () => {
expectedDigest,
expectedMessage,
expectedSignatures,
{ id: 'TOTP', token: '123456' },
)
})

Expand All @@ -204,8 +208,9 @@ describe('Guard Signer', () => {

const signatures = [mockHashSignature, mockEthSignSignature, mockErc1271Signature, mockSapientSignature]
const signedEnvelope = Envelope.toSigned(envelope, signatures)
const token = { id: 'TOTP' as const, code: '123456' }

const result = await guard.signEnvelope(signedEnvelope)
const result = await guard.signEnvelope(signedEnvelope, token)
expect(result).toEqual({
address: TEST_ADDRESS_1,
signature: {
Expand All @@ -227,6 +232,7 @@ describe('Guard Signer', () => {
expectedDigest,
expectedMessage,
expectedSignatures,
{ id: 'TOTP', token: '123456' },
)
})

Expand Down Expand Up @@ -263,8 +269,9 @@ describe('Guard Signer', () => {

const signatures = [mockHashSignature, mockEthSignSignature, mockErc1271Signature, mockSapientSignature]
const signedEnvelope = Envelope.toSigned(envelope, signatures)
const token = { id: 'TOTP' as const, code: '123456' }

const result = await guard.signEnvelope(signedEnvelope)
const result = await guard.signEnvelope(signedEnvelope, token)
expect(result).toEqual({
address: TEST_ADDRESS_1,
signature: {
Expand All @@ -285,6 +292,7 @@ describe('Guard Signer', () => {
expectedDigest,
expectedMessage,
expectedSignatures,
{ id: 'TOTP', token: '123456' },
)
})
})
8 changes: 5 additions & 3 deletions packages/wallet/dapp-client/src/ChainSessionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export class ChainSessionManager {
false,
implicitSession.loginMethod,
implicitSession.userEmail,
implicitSession.guard,
)
}

Expand Down Expand Up @@ -923,9 +924,10 @@ export class ChainSessionManager {
}
const signedEnvelope = Envelope.toSigned(envelope, [sapientSignature])

if (this.guard && !Envelope.reachedThreshold(signedEnvelope)) {
// TODO: this might fail if 2FA is required
const guard = new Signers.Guard(new Guard.Sequence.Guard(this.guard.url, this.guard.address))
if (!Envelope.reachedThreshold(signedEnvelope) && this.guard?.moduleAddresses.has(signature.address)) {
const guard = new Signers.Guard(
new Guard.Sequence.Guard(this.guard.url, this.guard.moduleAddresses.get(signature.address)!),
)
const guardSignature = await guard.signEnvelope(signedEnvelope)
signedEnvelope.signatures.push(guardSignature)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/dapp-client/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type LoginMethod = 'google' | 'apple' | 'email' | 'passkey' | 'mnemonic'

export interface GuardConfig {
url: string
address: Address.Address
moduleAddresses: Map<Address.Address, Address.Address>
}

// --- Payloads for Transport ---
Expand Down
46 changes: 46 additions & 0 deletions packages/wallet/wdk/src/sequence/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Address, Secp256k1 } from 'ox'
import { Shared } from './manager.js'
import * as Guard from '@0xsequence/guard'
import { Signers } from '@0xsequence/wallet-core'
import { Config } from '@0xsequence/wallet-primitives'

export enum GuardRole {
Wallet = 'wallet',
Sessions = 'sessions',
}

export class Guards {
constructor(private readonly shared: Shared) {}

getByRole(role: GuardRole) {
const guardAddress = this.shared.sequence.guardAddresses.get(role)
if (!guardAddress) {
throw new Error(`Guard address for role ${role} not found`)
}

return new Signers.Guard(new Guard.Sequence.Guard(this.shared.sequence.guardUrl, guardAddress))
}

getByAddress(address: Address.Address): [GuardRole, Signers.Guard] | undefined {
for (const [role, guardAddress] of this.shared.sequence.guardAddresses.entries()) {
if (guardAddress === address) {
return [role, this.getByRole(role)]
}
}
return undefined
}

topology(role: GuardRole): Config.NestedLeaf | undefined {
const guardAddress = this.shared.sequence.guardAddresses.get(role)
if (!guardAddress) {
return undefined
}

return {
type: 'nested',
weight: 1n,
threshold: 1n,
tree: { ...this.shared.sequence.defaultGuardTopology, address: guardAddress },
}
}
}
Loading