Skip to content

Commit

Permalink
feat: Onchain Identifier via transaction data and calldata fields (
Browse files Browse the repository at this point in the history
  • Loading branch information
DaniSomoza authored Dec 16, 2024
1 parent 1cff4b5 commit de4f1de
Show file tree
Hide file tree
Showing 14 changed files with 382 additions and 15 deletions.
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
7 changes: 7 additions & 0 deletions packages/protocol-kit/src/utils/getProtocolKitVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import packageJson from '../../package.json'

function getProtocolKitVersion(): string {
return packageJson.version
}

export default getProtocolKitVersion
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
Loading

0 comments on commit de4f1de

Please sign in to comment.