Skip to content
This repository was archived by the owner on Feb 8, 2026. It is now read-only.
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/vultisig/commondata v0.0.0-20250710214228-61d9ed8f7778
github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74
github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf
github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b
golang.org/x/sync v0.14.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,8 @@ github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74 h1:goqwk4n
github.com/vultisig/mobile-tss-lib v0.0.0-20250316003201-2e7e570a4a74/go.mod h1:nOykk4nOy1L3yXtLSlYvVsgizBnCQ3tR2N5uwGPdvaM=
github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070 h1:AcE6x2dxkl/4P/TxaMJ70pa2BcZ3+FJWnVmlPVAq4Dk=
github.com/vultisig/recipes v0.0.0-20250723142134-153c7f486070/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM=
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314 h1:817gEvSIW4cNqmQxZIpwttnuzaw3RYIGbHFtIsm9Eyk=
github.com/vultisig/recipes v0.0.0-20250724185247-fde1a4c02314/go.mod h1:30NOW5y2BnMCmEYFeVls3NhxkbMvuv7BNyRQfoFhGIM=
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf h1:3VhxFQR0oJEieaajNDvKlOGBDyTBbSMJSxow3zwUlmw=
github.com/vultisig/verifier v0.0.0-20250723150319-a51c59a884bf/go.mod h1:9f3yGSZWKZeXn30mU+/brufkbkLoGlFmNLKOpRbQgeI=
github.com/vultisig/vultiserver v0.0.0-20250715212748-4b23f9849e4b h1:Ed2DOWo8fA0KG6e36rzUmGpxcOQjmWTbxWyvUbI5by8=
Expand Down
183 changes: 76 additions & 107 deletions plugin/payroll/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,6 @@ func (p *Plugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.Plugi
return nil, fmt.Errorf("failed to get EVM ID for chain %s: %v", chain, err)
}

schedule := recipe.Schedule

var (
mu = &sync.Mutex{}
txs = make([]vtypes.PluginKeysignRequest, 0)
Expand All @@ -202,99 +200,71 @@ func (p *Plugin) ProposeTransactions(policy vtypes.PluginPolicy) ([]vtypes.Plugi
return nil, fmt.Errorf("getTokenID: %v", er)
}

recipients, amountStr, er := RuleToRecipientsAndAmount(rule)
recipient, amountStr, er := RuleToRecipientAndAmount(rule)
if er != nil {
return nil, fmt.Errorf("failed to get recipients and amount: %v", er)
return nil, fmt.Errorf("failed to get recipient and amount: %v", er)
}

for _, _recipient := range recipients {
recipient := _recipient

eg.Go(func() error {
isAlreadyProposed, e := p.IsAlreadyProposed(
ctx,
schedule.Frequency,
schedule.StartTime.AsTime(),
int(schedule.Interval),
chain,
policy.PluginID,
policy.ID,
tokenID,
recipient,
)
if e != nil {
return fmt.Errorf("p.IsAlreadyProposed: %w", e)
}
if isAlreadyProposed {
p.logger.WithFields(logrus.Fields{
"recipient": recipient,
"amount": amountStr,
"chain_id": chain,
"token_id": tokenID,
}).Info("transaction already proposed, skipping")
return nil
}

tx, e := p.genUnsignedTx(
ctx,
chain,
ethAddress,
tokenID,
amountStr,
recipient,
)
if e != nil {
return fmt.Errorf("p.genUnsignedTx: %w", e)
}

txHex := gcommon.Bytes2Hex(tx)

txData, e := ethereum.DecodeUnsignedPayload(tx)
if e != nil {
return fmt.Errorf("ethereum.DecodeUnsignedPayload: %w", e)
}
txHashToSign := etypes.LatestSignerForChainID(ethEvmID).Hash(etypes.NewTx(txData))

txToTrack, e := p.txIndexerService.CreateTx(ctx, storage.CreateTxDto{
PluginID: policy.PluginID,
PolicyID: policy.ID,
ChainID: chain,
TokenID: tokenID,
FromPublicKey: policy.PublicKey,
ToPublicKey: recipient,
ProposedTxHex: txHex,
})
if e != nil {
return fmt.Errorf("p.txIndexerService.CreateTx: %w", e)
}

msgHash := sha256.Sum256(txHashToSign.Bytes())

// Create signing request
signRequest := vtypes.PluginKeysignRequest{
KeysignRequest: vtypes.KeysignRequest{
PublicKey: policy.PublicKey,
Messages: []vtypes.KeysignMessage{
{
TxIndexerID: txToTrack.ID.String(),
Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()),
Chain: chain,
Hash: base64.StdEncoding.EncodeToString(msgHash[:]),
HashFunction: vtypes.HashFunction_SHA256,
},
eg.Go(func() error {
tx, e := p.genUnsignedTx(
ctx,
chain,
ethAddress,
tokenID,
amountStr,
recipient,
)
if e != nil {
return fmt.Errorf("p.genUnsignedTx: %w", e)
}

txHex := gcommon.Bytes2Hex(tx)

txData, e := ethereum.DecodeUnsignedPayload(tx)
if e != nil {
return fmt.Errorf("ethereum.DecodeUnsignedPayload: %w", e)
}
txHashToSign := etypes.LatestSignerForChainID(ethEvmID).Hash(etypes.NewTx(txData))

txToTrack, e := p.txIndexerService.CreateTx(ctx, storage.CreateTxDto{
PluginID: policy.PluginID,
PolicyID: policy.ID,
ChainID: chain,
TokenID: tokenID,
FromPublicKey: policy.PublicKey,
ToPublicKey: recipient,
ProposedTxHex: txHex,
})
if e != nil {
return fmt.Errorf("p.txIndexerService.CreateTx: %w", e)
}

msgHash := sha256.Sum256(txHashToSign.Bytes())

// Create signing request
signRequest := vtypes.PluginKeysignRequest{
KeysignRequest: vtypes.KeysignRequest{
PublicKey: policy.PublicKey,
Messages: []vtypes.KeysignMessage{
{
TxIndexerID: txToTrack.ID.String(),
Message: base64.StdEncoding.EncodeToString(txHashToSign.Bytes()),
Chain: chain,
Hash: base64.StdEncoding.EncodeToString(msgHash[:]),
HashFunction: vtypes.HashFunction_SHA256,
},
PolicyID: policy.ID,
PluginID: policy.PluginID.String(),
},
Transaction: txHex,
}
PolicyID: policy.ID,
PluginID: policy.PluginID.String(),
},
Transaction: txHex,
}

mu.Lock()
txs = append(txs, signRequest)
mu.Unlock()
return nil
})
}
mu.Lock()
txs = append(txs, signRequest)
mu.Unlock()
return nil
})
Comment thread
webpiratt marked this conversation as resolved.
}

err = eg.Wait()
Expand Down Expand Up @@ -391,7 +361,6 @@ func (p *Plugin) GetRecipeSpecification() (*rtypes.RecipeSchema, error) {
ParameterName: "recipient",
SupportedTypes: []rtypes.ConstraintType{
rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED,
rtypes.ConstraintType_CONSTRAINT_TYPE_WHITELIST,
},
Required: true,
},
Expand Down Expand Up @@ -457,38 +426,38 @@ func (p *Plugin) genUnsignedTx(
return nil, fmt.Errorf("unsupported chain: %s", chain)
}

func RuleToRecipientsAndAmount(rule *rtypes.Rule) ([]string, string, error) {
var recipients []string
var amountStr string

func RuleToRecipientAndAmount(rule *rtypes.Rule) (string, string, error) {
if len(rule.ParameterConstraints) == 0 {
return nil, "", fmt.Errorf("no parameter constraints found")
return "", "", fmt.Errorf("no parameter constraints found")
}

if len(rule.ParameterConstraints) > 3 {
return nil, "", fmt.Errorf("too many parameter constraints found")
return "", "", fmt.Errorf("too many parameter constraints found")
}

var (
recipient string
amountStr string
)
for _, constraint := range rule.ParameterConstraints {
if recipient != "" && amountStr != "" {
break
}

if constraint.ParameterName == "recipient" {
switch constraint.Constraint.Type {
case rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED:
recipients = []string{constraint.Constraint.GetFixedValue()}
case rtypes.ConstraintType_CONSTRAINT_TYPE_WHITELIST:
recipients = constraint.Constraint.GetWhitelistValues().GetValues()
if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED {
return "", "", fmt.Errorf("recipient constraint is not a fixed value")
}

recipient = constraint.Constraint.GetFixedValue()
}

if constraint.ParameterName == "amount" {
if constraint.Constraint.Type != rtypes.ConstraintType_CONSTRAINT_TYPE_FIXED {
return nil, "", fmt.Errorf("amount constraint is not a fixed value")
return "", "", fmt.Errorf("amount constraint is not a fixed value")
}

fixedValue := constraint.Constraint.GetValue().(*rtypes.Constraint_FixedValue)
amountStr = fixedValue.FixedValue
amountStr = constraint.Constraint.GetFixedValue()
}
}

return recipients, amountStr, nil
return recipient, amountStr, nil
}