Skip to content

Commit

Permalink
airdrop - admin cli txs (#1239)
Browse files Browse the repository at this point in the history
  • Loading branch information
sampocs authored Jul 12, 2024
1 parent 42b5ec4 commit d36b528
Show file tree
Hide file tree
Showing 9 changed files with 6,250 additions and 418 deletions.
186 changes: 182 additions & 4 deletions proto/stride/airdrop/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ package stride.airdrop;
import "cosmos/msg/v1/msg.proto";
import "amino/amino.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";

option go_package = "github.com/Stride-Labs/stride/v22/x/airdrop/types";

// Msg defines the Msg service.
service Msg {
// User facing messages:

// User transaction to claim all the pending daily airdrop rewards
rpc ClaimDaily(MsgClaimDaily) returns (MsgClaimDailyResponse);

Expand All @@ -23,9 +23,22 @@ service Msg {
// amount now. The funds can be unstaked by the user once the airdrop is over
rpc ClaimAndStake(MsgClaimAndStake) returns (MsgClaimAndStakeResponse);

// Admin facing messages:
// Admin transaction to create a new airdrop
rpc CreateAirdrop(MsgCreateAirdrop) returns (MsgCreateAirdropResponse);

// Admin transaction to update an existing airdrop
rpc UpdateAirdrop(MsgUpdateAirdrop) returns (MsgUpdateAirdropResponse);

// Admin transaction to add multiple user allocations for a given airdrop
rpc AddAllocations(MsgAddAllocations) returns (MsgAddAllocationsResponse);

// Admin transaction to update a user's allocation to an airdrop
rpc UpdateUserAllocation(MsgUpdateUserAllocation)
returns (MsgUpdateUserAllocationResponse);

// TODO
// Admin address to link a stride and non-stride address, merging their
// allocations
rpc LinkAddresses(MsgLinkAddresses) returns (MsgLinkAddressesResponse);
}

// ClaimDaily
Expand Down Expand Up @@ -65,3 +78,168 @@ message MsgClaimAndStake {
string validator_address = 3;
}
message MsgClaimAndStakeResponse {}

// CreateAirdrop
message MsgCreateAirdrop {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "airdrop/MsgCreateAirdrop";

// Airdrop admin address
string admin = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Airdrop ID
string airdrop_id = 2;

// The first date that claiming begins and rewards are distributed
google.protobuf.Timestamp distribution_start_date = 3
[ (gogoproto.stdtime) = true ];

// The last date for rewards to be distributed. Immediately after this date
// the rewards can no longer be claimed, but rewards have not been clawed back
// yet
google.protobuf.Timestamp distribution_end_date = 4
[ (gogoproto.stdtime) = true ];

// Date with which the rewards are clawed back (occurs after the distribution
// end date)
google.protobuf.Timestamp clawback_date = 5 [ (gogoproto.stdtime) = true ];

// Deadline for the user to make a decision on their claim type
google.protobuf.Timestamp claim_type_deadline_date = 6
[ (gogoproto.stdtime) = true ];

// Penalty for claiming rewards early - e.g. 0.5 means claiming early will
// result in losing 50% of rewards
string early_claim_penalty = 7 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Bonus rewards for choosing to claim and stake - e.g. 0.05 means stakers
// will receive a 5% bonus
string claim_and_stake_bonus = 8 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Address that holds the total reward balance and distributes to users
string distribution_address = 9
[ (cosmos_proto.scalar) = "cosmos.AddressString" ];
}
message MsgCreateAirdropResponse {}

// UpdateAirdrop
message MsgUpdateAirdrop {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "airdrop/MsgUpdateAirdrop";

// Airdrop admin address
string admin = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Airdrop ID
string airdrop_id = 2;

// The first date that claiming begins and rewards are distributed
google.protobuf.Timestamp distribution_start_date = 3
[ (gogoproto.stdtime) = true ];

// The last date for rewards to be distributed. Immediately after this date
// the rewards can no longer be claimed, but rewards have not been clawed back
// yet
google.protobuf.Timestamp distribution_end_date = 4
[ (gogoproto.stdtime) = true ];

// Date with which the rewards are clawed back (occurs after the distribution
// end date)
google.protobuf.Timestamp clawback_date = 5 [ (gogoproto.stdtime) = true ];

// Deadline for the user to make a decision on their claim type
google.protobuf.Timestamp claim_type_deadline_date = 6
[ (gogoproto.stdtime) = true ];

// Penalty for claiming rewards early - e.g. 0.5 means claiming early will
// result in losing 50% of rewards
string early_claim_penalty = 7 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Bonus rewards for choosing to claim and stake - e.g. 0.05 means stakers
// will receive a 5% bonus
string claim_and_stake_bonus = 8 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];

// Address that holds the total reward balance and distributes to users
string distribution_address = 9
[ (cosmos_proto.scalar) = "cosmos.AddressString" ];
}
message MsgUpdateAirdropResponse {}

// Allocation specification when bootstrapping reward data
message RawAllocation {
string user_address = 1;
repeated string allocations = 4 [
(gogoproto.nullable) = false,
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int"
];
}

// AddAllocations
message MsgAddAllocations {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "airdrop/MsgAddAllocations";

// Airdrop admin address
string admin = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Airdrop ID
string airdrop_id = 2;

// List of address/allocation pairs for each user
repeated RawAllocation allocations = 3 [ (gogoproto.nullable) = false ];
}
message MsgAddAllocationsResponse {}

// UpdateUserAllocation
message MsgUpdateUserAllocation {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "airdrop/MsgUpdateUserAllocation";

// Airdrop admin address
string admin = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Airdrop ID
string airdrop_id = 2;

// Address of the airdrop recipient
string user_address = 3 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Allocations - as an array where each element represents the rewards for a
// day
repeated string allocations = 4 [
(gogoproto.nullable) = false,
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int"
];
}
message MsgUpdateUserAllocationResponse {}

// LinkAddresses
message MsgLinkAddresses {
option (cosmos.msg.v1.signer) = "admin";
option (amino.name) = "airdrop/MsgLinkAddresses";

// Airdrop admin address
string admin = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];

// Airdrop ID
string airdrop_id = 2;

// Stride address - this address may or may not exist in allocations yet
string stride_address = 3;

// Host address - this address must exist
string host_address = 4 [ (cosmos_proto.scalar) = "cosmos.AddressString" ];
}
message MsgLinkAddressesResponse {}
95 changes: 95 additions & 0 deletions x/airdrop/client/cli/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package cli

import (
"bufio"
"encoding/csv"
"errors"
"fmt"
"os"
"strings"

sdkmath "cosmossdk.io/math"

"github.com/Stride-Labs/stride/v22/x/airdrop/types"
)

// Parses an allocations CSV file consisting of allocations for various addresses
//
// Example Schema:
//
// strideXXX,10,10,20
// strideYYY,0,10,0
func ParseMultipleUserAllocations(fileName string) ([]types.RawAllocation, error) {
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer file.Close()

reader := csv.NewReader(file)
rows, err := reader.ReadAll()
if err != nil {
return nil, err
}

allAllocations := []types.RawAllocation{}
for _, row := range rows {
if len(row) < 2 {
return nil, errors.New("invalid csv row")
}

userAddress := row[0]

allocations := []sdkmath.Int{}
for _, allocationString := range row[1:] {
allocation, ok := sdkmath.NewIntFromString(allocationString)
if !ok {
return nil, fmt.Errorf("unable to parse allocation %s into sdk.Int", allocationString)
}
allocations = append(allocations, allocation)
}

allocation := types.RawAllocation{
UserAddress: userAddress,
Allocations: allocations,
}
allAllocations = append(allAllocations, allocation)
}

return allAllocations, nil
}

// Parses a single user's allocations from a single line file with comma separate reward amounts
// Ex: 10,10,20
func ParseSingleUserAllocations(fileName string) (allocations []sdkmath.Int, err error) {
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
defer file.Close()

scanner := bufio.NewScanner(file)
var content string

if scanner.Scan() {
content = scanner.Text()
}

if scanner.Scan() {
return nil, fmt.Errorf("file %s has more than one line", fileName)
}
if err := scanner.Err(); err != nil {
return nil, err
}

allocationsSplit := strings.Split(content, ",")
for _, allocationString := range allocationsSplit {
allocation, ok := sdkmath.NewIntFromString(allocationString)
if !ok {
return nil, errors.New("unable to parse reward")
}
allocations = append(allocations, allocation)
}

return allocations, nil
}
75 changes: 75 additions & 0 deletions x/airdrop/client/cli/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package cli_test

import (
"os"
"testing"

sdkmath "cosmossdk.io/math"
"github.com/stretchr/testify/require"

"github.com/Stride-Labs/stride/v22/x/airdrop/client/cli"
"github.com/Stride-Labs/stride/v22/x/airdrop/types"
)

func ParseMultipleUserAllocations(t *testing.T) {
inputCSVContents := `strideXXX,10,10,20
strideYYY,0,10,0
strideZZZ,5,100,6`

expectedAllocations := []types.RawAllocation{
{
UserAddress: "strideXXX",
Allocations: []sdkmath.Int{sdkmath.NewInt(10), sdkmath.NewInt(10), sdkmath.NewInt(20)},
},
{
UserAddress: "strideYYY",
Allocations: []sdkmath.Int{sdkmath.NewInt(0), sdkmath.NewInt(10), sdkmath.NewInt(0)},
},
{
UserAddress: "strideZZZ",
Allocations: []sdkmath.Int{sdkmath.NewInt(5), sdkmath.NewInt(100), sdkmath.NewInt(6)},
},
}

// Create a temporary file
tmpfile, err := os.CreateTemp("", "allocations*.csv")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())

// Write the CSV string to the temp file
_, err = tmpfile.WriteString(inputCSVContents)
require.NoError(t, err)
err = tmpfile.Close()
require.NoError(t, err)

// Call the function with the temporary file name
actualAllocations, err := cli.ParseMultipleUserAllocations(tmpfile.Name())
require.NoError(t, err)

// Validate the allocations match expectations
require.Equal(t, expectedAllocations, actualAllocations)
}

func TestParseSingleUserAllocations(t *testing.T) {
inputCSVContents := "10,20,30"

expectedAllocations := []sdkmath.Int{sdkmath.NewInt(10), sdkmath.NewInt(20), sdkmath.NewInt(30)}

// Create a temporary file
tmpfile, err := os.CreateTemp("", "allocations*.csv")
require.NoError(t, err)
defer os.Remove(tmpfile.Name())

// Write the CSV string to the temp file
_, err = tmpfile.WriteString(inputCSVContents)
require.NoError(t, err)
err = tmpfile.Close()
require.NoError(t, err)

// Call the function with the temporary file name
actualAllocations, err := cli.ParseSingleUserAllocations(tmpfile.Name())
require.NoError(t, err)

// Validate the allocations match expectations
require.Equal(t, expectedAllocations, actualAllocations)
}
Loading

0 comments on commit d36b528

Please sign in to comment.