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

feat: Onchain Identifier via transaction data and calldata fields #1059

Merged
merged 15 commits into from
Dec 16, 2024
Merged
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
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
Loading