Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prepare release #1097

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ You will need to agree to [our CLA](https://safe.global/cla) in order to be poss

### Starting Guide

By following the steps bellow you will understand the development process and worflow.
By following the steps below you will understand the development process and workflow.
1. [Forking the repository](#forking-the-repository)
2. [Installing Node and Yarn](#installing-node-and-yarn)
3. [Installing dependencies](#installing-dependencies)
Expand All @@ -44,7 +44,7 @@ yarn -v

#### Installing dependencies

The Safe{Core} SDK uses a mono-repository structure managed by [Yarn Workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) and [Lerna](https://lerna.js.org). From the root of the repository you will need to install the whole dependency stack and do the project build. Some packages depend on each other, so even when modifiying only one package it's better to run the full build.
The Safe{Core} SDK uses a mono-repository structure managed by [Yarn Workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) and [Lerna](https://lerna.js.org). From the root of the repository you will need to install the whole dependency stack and do the project build. Some packages depend on each other, so even when modifying only one package it's better to run the full build.

Install all dependencies and build the whole project by using the following commands at the project root.

Expand Down
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021-2024 Safe Ecosystem Foundation
Copyright (c) 2021-2025 Safe Ecosystem Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
1 change: 1 addition & 0 deletions packages/protocol-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"format:check": "prettier --check \"*/**/*.{js,json,md,ts}\"",
"format": "prettier --write \"*/**/*.{js,json,md,ts}\"",
"unbuild": "rimraf dist artifacts deployments cache .nyc_output *.tsbuildinfo",
"prebuild": "node -p \"'export const getProtocolKitVersion = () => \\'' + require('./package.json').version.split('-')[0] + '\\''\" > src/utils/getProtocolKitVersion.ts",
"build": "yarn unbuild && yarn check-safe-deployments && NODE_OPTIONS=--max-old-space-size=8192 tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json"
},
"repository": {
Expand Down
76 changes: 72 additions & 4 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getChainSpecificDefaultSaltNonce,
getPredictedSafeAddressInitCode,
predictSafeAddress,
toTxResult,
validateSafeAccountConfig,
validateSafeDeploymentConfig
} from './contracts/utils'
Expand Down Expand Up @@ -83,9 +84,11 @@ import SafeMessage from './utils/messages/SafeMessage'
import semverSatisfies from 'semver/functions/satisfies'
import SafeProvider from './SafeProvider'
import { asHash, asHex } from './utils/types'
import { Hash, Hex } from 'viem'
import { Hash, Hex, SendTransactionParameters } from 'viem'
import getPasskeyOwnerAddress from './utils/passkeys/getPasskeyOwnerAddress'
import createPasskeyDeploymentTransaction from './utils/passkeys/createPasskeyDeploymentTransaction'
import generateOnChainIdentifier from './utils/on-chain-tracking/generateOnChainIdentifier'
import { getProtocolKitVersion } from './utils/getProtocolKitVersion'

const EQ_OR_GT_1_4_1 = '>=1.4.1'
const EQ_OR_GT_1_3_0 = '>=1.3.0'
Expand All @@ -102,6 +105,9 @@ class Safe {
#MAGIC_VALUE = '0x1626ba7e'
#MAGIC_VALUE_BYTES = '0x20c13b0b'

// on-chain Analytics
#onchainIdentifier: string = ''

/**
* Creates an instance of the Safe Core SDK.
* @param config - Ethers Safe configuration
Expand All @@ -126,7 +132,17 @@ class Safe {
* @throws "MultiSendCallOnly contract is not deployed on the current network"
*/
async #initializeProtocolKit(config: SafeConfig) {
const { provider, signer, isL1SafeSingleton, contractNetworks } = config
const { provider, signer, isL1SafeSingleton, contractNetworks, onchainAnalytics } = config

if (onchainAnalytics?.project) {
const { project, platform } = onchainAnalytics
this.#onchainIdentifier = generateOnChainIdentifier({
project,
platform,
tool: 'protocol-kit',
toolVersion: getProtocolKitVersion()
})
}

this.#safeProvider = await SafeProvider.init({
provider,
Expand Down Expand Up @@ -1340,6 +1356,32 @@ class Safe {

const signerAddress = await this.#safeProvider.getSignerAddress()

if (this.#onchainIdentifier) {
const encodedTransaction = await this.getEncodedTransaction(signedSafeTransaction)

const transaction = {
to: await this.getAddress(),
value: 0n,
data: encodedTransaction + this.#onchainIdentifier
}

const signer = await this.#safeProvider.getExternalSigner()

if (!signer) {
throw new Error('A signer must be set')
}

const hash = await signer.sendTransaction({
...transaction,
account: signer.account,
...options
} as SendTransactionParameters)

const provider = this.#safeProvider.getExternalProvider()

return toTxResult(provider, hash, options)
}

const txResponse = await this.#contractManager.safeContract.execTransaction(
signedSafeTransaction,
{
Expand Down Expand Up @@ -1466,6 +1508,14 @@ class Safe {
// we create the deployment transaction
const safeDeploymentTransaction = await this.createSafeDeploymentTransaction()

// remove the onchain idendifier if it is included
if (safeDeploymentTransaction.data.endsWith(this.#onchainIdentifier)) {
safeDeploymentTransaction.data = safeDeploymentTransaction.data.replace(
this.#onchainIdentifier,
''
)
}

// First transaction of the batch: The Safe deployment Transaction
const safeDeploymentBatchTransaction = {
to: safeDeploymentTransaction.to,
Expand All @@ -1486,7 +1536,11 @@ class Safe {
const transactions = [safeDeploymentBatchTransaction, safeBatchTransaction]

// this is the transaction with the batch
const safeDeploymentBatch = await this.createTransactionBatch(transactions, transactionOptions)
const safeDeploymentBatch = await this.createTransactionBatch(
transactions,
transactionOptions,
!!this.#onchainIdentifier // include the on chain identifier
)

return safeDeploymentBatch
}
Expand Down Expand Up @@ -1561,6 +1615,10 @@ class Safe {
])
}

if (this.#onchainIdentifier) {
safeDeployTransactionData.data += this.#onchainIdentifier
}

return safeDeployTransactionData
}

Expand All @@ -1572,12 +1630,14 @@ class Safe {
* @function createTransactionBatch
* @param {MetaTransactionData[]} transactions - An array of MetaTransactionData objects to be batched together.
* @param {TransactionOption} [transactionOptions] - Optional TransactionOption object to specify additional options for the transaction batch.
* @param {boolean} [includeOnchainIdentifier=false] - A flag indicating whether to append the onchain identifier to the data field of the resulting transaction.
* @returns {Promise<Transaction>} A Promise that resolves with the created transaction batch.
*
*/
async createTransactionBatch(
transactions: MetaTransactionData[],
transactionOptions?: TransactionOptions
transactionOptions?: TransactionOptions,
includeOnchainIdentifier: boolean = false
): Promise<Transaction> {
// we use the MultiSend contract to create the batch, see: https://github.com/safe-global/safe-contracts/blob/main/contracts/libraries/MultiSendCallOnly.sol
const multiSendCallOnlyContract = this.#contractManager.multiSendCallOnlyContract
Expand All @@ -1594,6 +1654,10 @@ class Safe {
data: batchData
}

if (includeOnchainIdentifier) {
transactionBatch.data += this.#onchainIdentifier
}

return transactionBatch
}

Expand Down Expand Up @@ -1701,6 +1765,10 @@ class Safe {
return getContractInfo(contractAddress)
}

getOnchainIdentifier(): string {
return this.#onchainIdentifier
}

/**
* This method creates a signer to be used with the init method
* @param {Credential} credential - The credential to be used to create the signer. Can be generated in the web with navigator.credentials.create
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
} from './utils/eip-712'
import { createPasskeyClient } from './utils/passkeys/PasskeyClient'
import getPasskeyOwnerAddress from './utils/passkeys/getPasskeyOwnerAddress'
import generateOnChainIdentifier from './utils/on-chain-tracking/generateOnChainIdentifier'

export {
estimateTxBaseGas,
Expand All @@ -80,6 +81,7 @@ export {
EthSafeSignature,
MultiSendCallOnlyBaseContract,
MultiSendBaseContract,
generateOnChainIdentifier,
PREDETERMINED_SALT_NONCE,
SafeBaseContract,
SafeProxyFactoryBaseContract,
Expand Down
11 changes: 11 additions & 0 deletions packages/protocol-kit/src/types/safeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,22 @@ type SafeConfigWithPredictedSafeProps = {
predictedSafe: PredictedSafeProps
}

export type OnchainAnalyticsProps = {
/** project - The project that is using the SDK */
project?: string
/** platform - The platform that is using the SDK */
platform?: string
}

export type SafeConfigProps = {
provider: SafeProviderConfig['provider']
signer?: SafeProviderConfig['signer']
/** isL1SafeSingleton - Forces to use the Safe L1 version of the contract instead of the L2 version */
isL1SafeSingleton?: boolean
/** contractNetworks - Contract network configuration */
contractNetworks?: ContractNetworksConfig
// on-chain analytics
onchainAnalytics?: OnchainAnalyticsProps
}

export type SafeConfigWithSafeAddress = SafeConfigProps & SafeConfigWithSafeAddressProps
Expand Down Expand Up @@ -75,6 +84,8 @@ type ConnectSafeConfigProps = {
isL1SafeSingleton?: boolean
/** contractNetworks - Contract network configuration */
contractNetworks?: ContractNetworksConfig
// on-chain analytics
onchainAnalytics?: OnchainAnalyticsProps
}

export type ConnectSafeConfigWithSafeAddress = ConnectSafeConfigProps &
Expand Down
1 change: 1 addition & 0 deletions packages/protocol-kit/src/utils/getProtocolKitVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getProtocolKitVersion = () => '5.1.1'
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { keccak256, toHex } from 'viem'

/**
* Generates a hash from the given input string and truncates it to the specified size.
*
* @param {string} input - The input string to be hashed.
* @param {number} size - The number of bytes to take from the end of the hash.
* @returns {string} A hexadecimal string representation of the truncated hash, without the `0x` prefix.
*/
export function generateHash(input: string, size: number): string {
const fullHash = keccak256(toHex(input))
return toHex(fullHash.slice(-size)).replace('0x', '') // Take the last X bytes
}

export type OnChainIdentifierParamsType = {
project: string
platform?: string
tool: string
toolVersion: string
}

/**
* Generates an on-chain identifier for tracking transactions on the blockchain.
* This identifier includes hashed metadata such as the project name, platform, tool, and tool version.
*
* @param {Object} params - An object containing the metadata for generating the on-chain identifier.
* @param {string} params.project - The name of the project initiating the transaction.
* @param {string} [params.platform='Web'] - The platform from which the transaction originates (e.g., "Web", "Mobile", "Safe App", "Widget"...).
* @param {string} params.tool - The tool used to generate the transaction (e.g., "protocol-kit").
* @param {string} params.toolVersion - The version of the tool used to generate the transaction.
* @returns {string} A string representing the on-chain identifier, composed of multiple hashed segments.
*
* @example
* const identifier = generateOnChainIdentifier({
* project: 'MyProject',
* platform: 'Mobile',
* tool: 'protocol-kit',
* toolVersion: '4.0.0'
* })
*/
function generateOnChainIdentifier({
project,
platform = 'Web',
tool,
toolVersion
}: OnChainIdentifierParamsType): string {
const identifierPrefix = '5afe'
const identifierVersion = '00' // first version
const projectHash = generateHash(project, 20) // Take the last 20 bytes
const platformHash = generateHash(platform, 3) // Take the last 3 bytes
const toolHash = generateHash(tool, 3) // Take the last 3 bytes
const toolVersionHash = generateHash(toolVersion, 3) // Take the last 3 bytes

return `${identifierPrefix}${identifierVersion}${projectHash}${platformHash}${toolHash}${toolVersionHash}`
}

export default generateOnChainIdentifier
12 changes: 9 additions & 3 deletions packages/protocol-kit/src/utils/passkeys/PasskeyClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,11 @@ function extractClientDataFields(clientDataJSON: ArrayBuffer): Hex {
* Extracts the numeric values r and s from a DER-encoded ECDSA signature.
* This function decodes the signature based on a specific format and validates the encoding at each step.
*
* @param {ArrayBuffer} signature - The DER-encoded signature to be decoded.
* @param {ArrayBuffer | Uint8Array | Array<number>} signature - The DER-encoded signature to be decoded. The WebAuthn standard expects the signature to be an ArrayBuffer, but some password managers (including Bitwarden) provide a Uint8Array or an array of numbers instead.
* @returns {[bigint, bigint]} A tuple containing two BigInt values, r and s, which are the numeric values extracted from the signature.
* @throws {Error} Throws an error if the signature encoding is invalid or does not meet expected conditions.
*/
function extractSignature(signature: ArrayBuffer): [bigint, bigint] {
function extractSignature(signature: ArrayBuffer | Uint8Array | Array<number>): [bigint, bigint] {
const check = (x: boolean) => {
if (!x) {
throw new Error('invalid signature encoding')
Expand All @@ -214,7 +214,13 @@ function extractSignature(signature: ArrayBuffer): [bigint, bigint] {
// Decode the DER signature. Note that we assume that all lengths fit into 8-bit integers,
// which is true for the kinds of signatures we are decoding but generally false. I.e. this
// code should not be used in any serious application.
const view = new DataView(signature)
const view = new DataView(
signature instanceof ArrayBuffer
? signature
: signature instanceof Uint8Array
? signature.buffer
: new Uint8Array(signature).buffer
)

// check that the sequence header is valid
check(view.getUint8(0) === 0x30)
Expand Down
Loading
Loading