Skip to content

Instantly share code, notes, and snippets.

@nikhilsaraf
Last active May 24, 2021 17:15
Show Gist options
  • Save nikhilsaraf/ff3ae46116b6ae6dbdcd1743ad9495ec to your computer and use it in GitHub Desktop.
Save nikhilsaraf/ff3ae46116b6ae6dbdcd1743ad9495ec to your computer and use it in GitHub Desktop.
Stellar URI Scheme Request Generation and Signing
/*
Copyright 2018 Lightyear.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"encoding/base64"
"log"
"net/url"
"github.com/stellar/go/keypair"
)
func main() {
const stellarPrivateKey = "SBPOVRVKTTV7W3IOX2FJPSMPCJ5L2WU2YKTP3HCLYPXNI5MDIGREVNYC"
const stellarPublicKey = "GD7ACHBPHSC5OJMJZZBXA7Z5IAUFTH6E6XVLNBPASDQYJ7LO5UIYBDQW"
// data is your payload
data := "web+stellar:pay?destination=GCALNQQBXAPZ2WIRSDDBMSTAKCUH5SG6U76YBFLQLIXJTF7FE5AX7AOO&amount=120.1234567&memo=skdjfasf&msg=pay%20me%20with%20lumens&origin_domain=someDomain.com"
// sign it
urlEncodedBase64Signature := sign(data, stellarPrivateKey)
log.Println("url-encoded base64 signature:", urlEncodedBase64Signature)
// verify the signature
e := verify(data, urlEncodedBase64Signature, stellarPublicKey)
if e != nil {
log.Fatal(e)
}
log.Println("data is valid")
// append signature to original URI request
log.Printf("signed URI request: %s&signature=%s\n", data, urlEncodedBase64Signature)
}
// -------------------------------------------------------------------------
// ---------------------------- P A Y L O A D ------------------------------
// -------------------------------------------------------------------------
func constuctPayload(data string) []byte {
// prefix 4 to denote application-based signing using 36 bytes
var prefixSelectorBytes [36]byte
prefixSelectorBytes = [36]byte{}
prefixSelectorBytes[35] = 4
// standardized namespace prefix for this signing use case
prefix := "stellar.sep.7 - URI Scheme"
// variable number of bytes for the prefix + data
var uriWithPrefixBytes []byte
uriWithPrefixBytes = []byte(prefix + data)
var result []byte
result = append(result, prefixSelectorBytes[:]...) // 36 bytes
result = append(result, uriWithPrefixBytes[:]...) // variable length bytes
return result
}
// -------------------------------------------------------------------------
// ---------------------------- S I G N I N G ------------------------------
// -------------------------------------------------------------------------
func sign(data string, stellarPrivateKey string) string {
// construct the payload
payloadBytes := constuctPayload(data)
// sign the data
kp := keypair.MustParse(stellarPrivateKey)
signatureBytes, e := kp.Sign(payloadBytes)
if e != nil {
log.Fatal(e)
}
// encode the signature as base64
base64Signature := base64.StdEncoding.EncodeToString(signatureBytes)
log.Println("base64 signature:", base64Signature)
// url-encode it
urlEncodedBase64Signature := url.QueryEscape(base64Signature)
return urlEncodedBase64Signature
}
// -------------------------------------------------------------------------
// ------------------------- V E R I F I C A T I O N -----------------------
// -------------------------------------------------------------------------
func verify(data string, urlEncodedBase64Signature string, stellarPublicKey string) error {
// construct the payload so we can verify it
payloadBytes := constuctPayload(data)
// decode the url-encoded signature
kp := keypair.MustParse(stellarPublicKey)
base64Signature, e := url.QueryUnescape(urlEncodedBase64Signature)
if e != nil {
log.Fatal(e)
}
// decode the base64 signature
signatureBytes, e := base64.StdEncoding.DecodeString(base64Signature)
if e != nil {
log.Fatal(e)
}
// validate it against the public key
return kp.Verify(payloadBytes, signatureBytes)
}
/*
Copyright 2018 Lightyear.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"fmt"
"log"
"net/url"
"os"
"strings"
b "github.com/stellar/go/build"
kp "github.com/stellar/go/keypair"
)
func main() {
destinationAddress, memo, creditAmount := parseInputs()
// 1. build the partial transaction (excludes the source account and sequence number)
emptyAddress := kp.Master("").Address()
txn, e := b.Transaction(
// since the address is the empty sentinel value, the wallet will need to fill it in along with the sequence number
b.SourceAccount{AddressOrSeed: emptyAddress},
// meaningless to have a sequence number here since the source account is the empty address and will be replaced by the wallet
b.TestNetwork,
b.Payment(
b.Destination{AddressOrSeed: destinationAddress},
creditAmount,
),
)
if e != nil {
log.Fatal(e)
}
if memo != "" {
e = txn.Mutate(b.MemoText{Value: memo})
if e != nil {
log.Fatal(e)
}
}
// 2. sign with empty signature so it gets converted to a transaction envelope
txnE, e := txn.Sign()
if e != nil {
log.Fatal("failed to sign: ", e)
}
// 3. convert to base64
txnB64, e := txnE.Base64()
if e != nil {
log.Fatal("failed to convert to base64: ", e)
}
// 4. url encode
urlEncoded := url.QueryEscape(txnB64)
fmt.Println("web+stellar:tx?xdr=" + urlEncoded)
}
// boilerplate to parse command line args and to make this implementation functional
func parseInputs() (destinationAddress string, memo string, creditAmount b.PaymentMutator) {
toAddressPtr := flag.String("toAddress", "", "destination address")
amountPtr := flag.Float64("amount", 0.0, "amount to be sent, must be > 0.0")
memoPtr := flag.String("memo", "", "(optional) memo to include with the payment")
assetPtr := flag.String("asset", "", "(optional) asset to pay with, of the form code:issuer")
flag.Parse()
if *toAddressPtr == "" || *amountPtr <= 0 {
fmt.Println("Params:")
flag.PrintDefaults()
os.Exit(1)
}
amountStr := fmt.Sprintf("%v", *amountPtr)
if *assetPtr != "" {
assetParts := strings.SplitN(*assetPtr, ":", 2)
issuerAddress := assetParts[1]
creditAmount = b.CreditAmount{Code: assetParts[0], Issuer: issuerAddress, Amount: amountStr}
} else {
creditAmount = b.NativeAmount{Amount: amountStr}
}
return *toAddressPtr, *memoPtr, creditAmount
}
/*
Copyright 2018 Lightyear.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"log"
"bufio"
"os"
"strings"
b "github.com/stellar/go/build"
"github.com/stellar/go/xdr"
)
// sample reference implementation to collate signatures using multiple signed transactions for a multi-signature coordination service
func main() {
xdrList := []string{}
fmt.Printf("enter the first signed base64-encoded transaction xdr:\n")
for {
reader := bufio.NewReader(os.Stdin)
tx, _ := reader.ReadString('\n')
tx = strings.Replace(tx, "\n", "", -1)
if len(tx) == 0 {
fmt.Printf("received empty tx xdr, done entering transactions.\n")
break
}
xdrList = append(xdrList, tx)
fmt.Printf("\nenter the next signed base64-encoded transaction xdr (enter to continue):\n")
}
combinedTx := collate(xdrList)
fmt.Printf("\n\ncollated transaction:\n%s\n", combinedTx)
}
// collate takes the list of base64-encoded transaction XDRs and combines the signatures to produce a single transaction XDR.
// in order to combine signatures, collate needs to verify that each transaction is the same.
func collate(xdrList []string) string {
// we will use collated to collate all the transactions
collated := decodeFromBase64(xdrList[0])
for _, xdr := range xdrList[1:] {
tx := decodeFromBase64(xdr)
// implementations should take precautions before combining signatures, including but not limited to: deduping signatures, verifying signatures, checking that the transactions are the same, etc.
collated.E.Signatures = append(collated.E.Signatures, tx.E.Signatures...)
}
collatedXdr, e := collated.Base64()
if e != nil {
log.Fatal("failed to convert to base64:", e)
}
return collatedXdr
}
// decodeFromBase64 decodes the transaction from a base64 string into a TransactionEnvelopeBuilder
func decodeFromBase64(encodedXdr string) *b.TransactionEnvelopeBuilder {
// Unmarshall from base64 encoded XDR format
var decoded xdr.TransactionEnvelope
e := xdr.SafeUnmarshalBase64(encodedXdr, &decoded)
if e != nil {
log.Fatal(e)
}
// convert to TransactionEnvelopeBuilder
txEnvelopeBuilder := b.TransactionEnvelopeBuilder{E: &decoded}
txEnvelopeBuilder.Init()
return &txEnvelopeBuilder
}
/*
Copyright 2018 Lightyear.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"flag"
"fmt"
"log"
"net/url"
"os"
b "github.com/stellar/go/build"
"github.com/stellar/go/clients/horizon"
"github.com/stellar/go/xdr"
kp "github.com/stellar/go/keypair"
)
var emptyAddress = kp.Master("").Address()
func main() {
secretKey, uriString := parseInputs()
// 1. extract the URL-encoded xdr string from URI
uri, e := url.ParseRequestURI(uriString)
if e != nil {
log.Fatal(e)
}
encodedInputTxn := uri.Query().Get("xdr")
// 2. decode the Transaction
unescapedTxn := unescape(encodedInputTxn)
// 3. decode the base64 XDR
txn := decodeFromBase64(unescapedTxn)
// 4. check the source account and mutate the transaction inside the transaction envelope if needed:
// a. update the source account
// b. set the sequence number
// c. set the network passphrase
horizonClient := horizon.DefaultTestNetClient
if txn.E.Tx.SourceAccount.Address() == emptyAddress {
e = txn.MutateTX(
// we assume that the accountID uses the master key, this can also be the accountID
&b.SourceAccount{AddressOrSeed: secretKey},
&b.AutoSequence{SequenceProvider: horizonClient},
// need to reset the network passphrase
b.TestNetwork,
)
if e != nil {
log.Fatal(e)
}
} else if txn.E.Tx.SeqNum == 0 {
e = txn.MutateTX(
// do not need to set the source account here, only the sequence number
&b.AutoSequence{SequenceProvider: horizonClient},
// need to reset the network passphrase
b.TestNetwork,
)
if e != nil {
log.Fatal(e)
}
}
// 5. sign the transaction envelope
e = txn.Mutate(&b.Sign{Seed: secretKey})
if e != nil {
log.Fatal(e)
}
// 6. convert the transaction to base64
reencodedTxnBase64, e := txn.Base64()
if e != nil {
log.Fatal("failed to convert to base64: ", e)
}
// 7. submit to the network
resp, e := horizonClient.SubmitTransaction(reencodedTxnBase64)
if e != nil {
log.Fatal(e)
}
fmt.Println("transaction posted in ledger:", resp.Ledger)
}
// unescape decodes the URL-encoded and base64 encoded txn
func unescape(escaped string) string {
unescaped, e := url.QueryUnescape(escaped)
if e != nil {
log.Fatal(e)
}
return unescaped
}
// decodeFromBase64 decodes the transaction from a base64 string into a TransactionEnvelopeBuilder
func decodeFromBase64(encodedXdr string) *b.TransactionEnvelopeBuilder {
// Unmarshall from base64 encoded XDR format
var decoded xdr.TransactionEnvelope
e := xdr.SafeUnmarshalBase64(encodedXdr, &decoded)
if e != nil {
log.Fatal(e)
}
// convert to TransactionEnvelopeBuilder
txEnvelopeBuilder := b.TransactionEnvelopeBuilder{E: &decoded}
txEnvelopeBuilder.Init()
return &txEnvelopeBuilder
}
// boilerplate to parse command line args and to make this implementation functional
func parseInputs() (secretKey string, uriString string) {
// assumes that the signing account uses only the master key to sign transactions
secretKeyPtr := flag.String("secretKey", "", "secret key to sign the transaction")
uriPtr := flag.String("uri", "", "URI Request that contains the XDR Transaction to be signed and submitted, only supports a limited set of operations for SEP7")
flag.Parse()
if *secretKeyPtr == "" || *uriPtr == "" {
fmt.Println("Params:")
flag.PrintDefaults()
os.Exit(1)
}
return *secretKeyPtr, *uriPtr
}
Copyright 2018 Lightyear.io

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Sample commands to run it:

stellar_gen_uri_request -amount 1.1234567 -toAddress GAQUH57O777R3LPOFNHMKC6C67PWQOC2UI3G34JYIDZAW4OUNJWJKOIJ

stellar_sign_uri_txn -uri web+stellar:tx?xdr=AAAAAL6Qe0ushP7lzogR2y3vyb8LKiorvD1U2KIlfs1wRBliAAAAZAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAIUP37v%2F%2FHa3uK07FC8L332g4WqI2bfE4QPILcdRqbJUAAAAAAAAAAACrbQcAAAAAAAAAAA%3D%3D -secretKey SANVFFZ6IXB7LBEVG6DPXL6ON2UZFOM3M7FW57YII3XLB2BQUZ444TT6

<!--
Copyright 2018 Lightyear.io
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<title>Stellar Payment Request Handler</title>
<script type="text/javascript">
// register as a handler for the "web+stellar" scheme
// redirect url needs to be on the same domain of this current page (assuming it is hosted on localhost)
window.navigator.registerProtocolHandler("web+stellar", "http://localhost:8000/webHandler.html?req=%s", "My Web Wallet");
window.onload = function() {
// get the payment request url from our handler's url
var url_string = window.location.href;
var url = new URL(url_string);
var req_url_string = url.searchParams.get("req");
console.log(req_url_string)
document.getElementById("req").innerText = req_url_string;
// extract the xdr from the payment request
var req_url = new URL(req_url_string);
var xdr_string = req_url.searchParams.get("xdr");
console.log(xdr_string)
document.getElementById("xdr").innerText = xdr_string;
}
</script>
</head>
<body>
Stellar payment request URI:
<p id="req"></p>
<br/>
<br/>
xdr to be signed:
<p id="xdr"></p>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment