Skip to content

Commit

Permalink
Add mint v1 support (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
evlekht authored Oct 2, 2024
1 parent ccc84c2 commit d2ad06d
Show file tree
Hide file tree
Showing 7 changed files with 544 additions and 340 deletions.
5 changes: 4 additions & 1 deletion examples/booking/mintnbuy.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func main() {
}

bt, err := bookingtoken.NewBookingtoken(common.HexToAddress("0xe55E387F5474a012D1b048155E25ea78C7DBfBBC"), client)
if err != nil {
sugar.Fatalf("Failed to create BookingToken contract binding: %v", err)
}

// token uri
tokenURI := "data:application/json;base64,eyJuYW1lIjoiYm90IGNtYWNjb3VudCBwa2cgYm9va2luZyB0b2tlbiB0ZXN0In0K"
Expand Down Expand Up @@ -193,7 +196,7 @@ func main() {

switch price.Currency.Currency.(type) {
case *typesv2.Currency_NativeToken:
bigIntPrice, err = bs.ConvertPriceToBigInt(price, int32(18)) // CAM uses 18 decimals
bigIntPrice, err = bs.ConvertPriceToBigInt(price.Value, price.Decimals, int32(18)) // CAM uses 18 decimals
if err != nil {
sugar.Errorf("Failed to convert price to big.Int: %v", err)
return
Expand Down
6 changes: 6 additions & 0 deletions examples/rpc/partner-plugin/handlers/mint_v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
typesv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/types/v1"
"github.com/chain4travel/camino-messenger-bot/internal/metadata"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)

Expand All @@ -37,6 +38,11 @@ func (*MintServiceV1Server) Mint(ctx context.Context, _ *bookv1.MintRequest) (*b
Price: &typesv1.Price{
Value: "1",
Decimals: 9,
Currency: &typesv1.Currency{
Currency: &typesv1.Currency_NativeToken{
NativeToken: &emptypb.Empty{},
},
},
},
}

Expand Down
241 changes: 241 additions & 0 deletions internal/messaging/mint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package messaging

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"time"

notificationv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/services/notification/v1"
typesv1 "buf.build/gen/go/chain4travel/camino-messenger-protocol/protocolbuffers/go/cmp/types/v1"
"github.com/chain4travel/camino-messenger-contracts/go/contracts/bookingtoken"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethTypes "github.com/ethereum/go-ethereum/core/types"
"google.golang.org/grpc"
grpc_metadata "google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/timestamppb"
)

// Mints a BookingToken with the supplier private key and reserves it for the buyer address
// For testing you can use this uri: "data:application/json;base64,eyJuYW1lIjoiQ2FtaW5vIE1lc3NlbmdlciBCb29raW5nVG9rZW4gVGVzdCJ9Cg=="
func (h *evmResponseHandler) mint(
ctx context.Context,
reservedFor common.Address,
uri string,
expiration *big.Int,
price *big.Int,
paymentToken common.Address,
) (string, *big.Int, error) {
// TODO:
// (in booking package)
// define paymentToken from currency
// if TokenCurrency get paymentToken contract and call decimals()
// calculate the price in big int without loosing precision

tx, err := h.bookingService.MintBookingToken(
reservedFor,
uri,
expiration,
price,
paymentToken)
if err != nil {
return "", nil, err
}

// Wait for transaction to be mined
receipt, err := bind.WaitMined(ctx, h.ethClient, tx)
if err != nil {
return "", nil, err
}

tokenID := big.NewInt(0)

for _, mLog := range receipt.Logs {
event, err := h.bookingToken.ParseTokenReserved(*mLog)
if err == nil {
tokenID = event.TokenId
h.logger.Infof("[TokenReserved] TokenID: %s ReservedFor: %s Price: %s, PaymentToken: %s", event.TokenId, event.ReservedFor, event.Price, event.PaymentToken)
}
}

return tx.Hash().Hex(), tokenID, nil
}

// TODO @VjeraTurk code that creates and handles context should be improved, since its not doing job in separate goroutine,
// Buys a token with the buyer private key. Token must be reserved for the buyer address.
func (h *evmResponseHandler) buy(ctx context.Context, tokenID *big.Int) (string, error) {
tx, err := h.bookingService.BuyBookingToken(tokenID)
if err != nil {
return "", err
}

receipt, err := h.waitTransaction(ctx, tx)
if err != nil {
return "", err
}
if receipt.Status != ethTypes.ReceiptStatusSuccessful {
return "", fmt.Errorf("transaction failed: %v", receipt)
}

h.logger.Infof("Transaction sent!\nTransaction hash: %s\n", tx.Hash().Hex())

return tx.Hash().Hex(), nil
}

func (h *evmResponseHandler) onBookingTokenMint(tokenID *big.Int, mintID *typesv1.UUID, buyableUntil time.Time) {
notificationClient := h.serviceRegistry.NotificationClient()
expirationTimer := &time.Timer{}

unsubscribeTokenBought, err := h.evmEventListener.RegisterTokenBoughtHandler(
h.bookingTokenAddress,
[]*big.Int{tokenID},
nil,
func(e any) {
expirationTimer.Stop()
h.logger.Infof("Token bought event received for token %s", tokenID.String())
event := e.(*bookingtoken.BookingtokenTokenBought)

if _, err := notificationClient.TokenBoughtNotification(
context.Background(),
&notificationv1.TokenBought{
TokenId: tokenID.Uint64(),
TxId: event.Raw.TxHash.Hex(),
MintId: mintID,
},
grpc.Header(&grpc_metadata.MD{}),
); err != nil {
h.logger.Errorf("error calling partner plugin TokenBoughtNotification service: %v", err)
}
},
)
if err != nil {
h.logger.Errorf("failed to register handler: %v", err)
// TODO @evlekht send some notification to partner plugin
return
}

expirationTimer = time.AfterFunc(time.Until(buyableUntil), func() {
unsubscribeTokenBought()
h.logger.Infof("Token %s expired", tokenID.String())

if _, err := notificationClient.TokenExpiredNotification(
context.Background(),
&notificationv1.TokenExpired{
TokenId: tokenID.Uint64(),
MintId: mintID,
},
grpc.Header(&grpc_metadata.MD{}),
); err != nil {
h.logger.Errorf("error calling partner plugin TokenExpiredNotification service: %v", err)
}
})
}

// TODO @evlekht check if those structs are needed as exported here, otherwise make them private or move to another pkg
type hotelAtrribute struct {
TraitType string `json:"trait_type"`
Value string `json:"value"`
}

type hotelJSON struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Date string `json:"date,omitempty"`
ExternalURL string `json:"external_url,omitempty"`
Image string `json:"image,omitempty"`
Attributes []hotelAtrribute `json:"attributes,omitempty"`
}

// Generates a token data URI from a MintResponse object. Returns jsonPlain and a
// data URI with base64 encoded json data.
//
// TODO: @havan: We need decide what data needs to be in the tokenURI JSON and add
// those fields to the MintResponse. These will be shown in the UI of wallets,
// explorers etc.
func createTokenURIforMintResponse(mintID, bookingReference string) (string, string, error) {
// TODO: What should we use for a token name? This will be shown in the UI of wallets, explorers etc.
name := "CM Booking Token"

// TODO: What should we use for a token description? This will be shown in the UI of wallets, explorers etc.
description := "This NFT represents the booking with the specified attributes."

// Dummy data
date := "2024-09-27"

externalURL := "https://camino.network"

// Placeholder Image
image := "https://camino.network/static/images/N9IkxmG-Sg-1800.webp"

attributes := []hotelAtrribute{
{
TraitType: "Mint ID",
Value: mintID,
},
{
TraitType: "Reference",
Value: bookingReference,
},
}

jsonPlain, jsonEncoded, err := generateAndEncodeJSON(
name,
description,
date,
externalURL,
image,
attributes,
)
if err != nil {
return "", "", err
}

// Add data URI scheme
tokenURI := "data:application/json;base64," + jsonEncoded

return jsonPlain, tokenURI, nil
}

func generateAndEncodeJSON(name, description, date, externalURL, image string, attributes []hotelAtrribute) (string, string, error) {
hotel := hotelJSON{
Name: name,
Description: description,
Date: date,
ExternalURL: externalURL,
Image: image,
Attributes: attributes,
}

jsonData, err := json.Marshal(hotel)
if err != nil {
return "", "", err
}

encoded := base64.StdEncoding.EncodeToString(jsonData)
return string(jsonData), encoded, nil
}

func verifyAndFixBuyableUntil(buyableUntil *timestamppb.Timestamp, currentTime time.Time) (*timestamppb.Timestamp, error) {
switch {
case buyableUntil == nil || buyableUntil.Seconds == 0:
// BuyableUntil not set
return timestamppb.New(currentTime.Add(buyableUntilDurationDefault)), nil

case buyableUntil.Seconds < timestamppb.New(currentTime).Seconds:
// BuyableUntil in the past
return nil, fmt.Errorf("refused to mint token - BuyableUntil in the past: %v", buyableUntil)

case buyableUntil.Seconds < timestamppb.New(currentTime.Add(buyableUntilDurationMinimal)).Seconds:
// BuyableUntil too early
return timestamppb.New(currentTime.Add(buyableUntilDurationMinimal)), nil

case buyableUntil.Seconds > timestamppb.New(currentTime.Add(buyableUntilDurationMaximal)).Seconds:
// BuyableUntil too late
return timestamppb.New(currentTime.Add(buyableUntilDurationMaximal)), nil
}

return buyableUntil, nil
}
Loading

0 comments on commit d2ad06d

Please sign in to comment.