github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/stellar/stellarsvc/anchor_test.go (about)

     1  package stellarsvc
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"net/url"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/stellar1"
    13  	"github.com/keybase/stellarnet"
    14  	"github.com/stellar/go/build"
    15  )
    16  
    17  type anchorTest struct {
    18  	Name                string
    19  	Asset               stellar1.Asset
    20  	DepositExternalURL  string
    21  	WithdrawExternalURL string
    22  	DepositMessage      string
    23  	WithdrawMessage     string
    24  	MockTransferGet     func(mctx libkb.MetaContext, url, authToken string) (int, []byte, error)
    25  }
    26  
    27  var errAnchorTests = []anchorTest{
    28  	{
    29  		Name: "not verified",
    30  		Asset: stellar1.Asset{
    31  			Type:   "credit_alphanum4",
    32  			Code:   "EUR",
    33  			Issuer: "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    34  		},
    35  		MockTransferGet: mockKeybaseTransferGet,
    36  	},
    37  	{
    38  		Name: "no transfer server",
    39  		Asset: stellar1.Asset{
    40  			Type:           "credit_alphanum4",
    41  			Code:           "EUR",
    42  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    43  			VerifiedDomain: "keybase.io",
    44  		},
    45  		MockTransferGet: mockKeybaseTransferGet,
    46  	},
    47  	{
    48  		Name: "requires auth but with different domain",
    49  		Asset: stellar1.Asset{
    50  			Type:           "credit_alphanum4",
    51  			Code:           "EUR",
    52  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    53  			VerifiedDomain: "keybase.io",
    54  			TransferServer: "https://transfer.keybase.io/transfer",
    55  			AuthEndpoint:   "https://transfer.keycase.io/auth",
    56  		},
    57  		MockTransferGet: mockKeybaseTransferGet,
    58  	},
    59  	{
    60  		Name: "invalid url",
    61  		Asset: stellar1.Asset{
    62  			Type:           "credit_alphanum4",
    63  			Code:           "EUR",
    64  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    65  			VerifiedDomain: "keybase.io",
    66  			TransferServer: ":transfer.keybase.io/transfer",
    67  		},
    68  		MockTransferGet: mockKeybaseTransferGet,
    69  	},
    70  	{
    71  		Name: "different host",
    72  		Asset: stellar1.Asset{
    73  			Type:           "credit_alphanum4",
    74  			Code:           "EUR",
    75  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    76  			VerifiedDomain: "keybase.io",
    77  			TransferServer: "https://transfer.keybays.io/transfer",
    78  		},
    79  		MockTransferGet: mockKeybaseTransferGet,
    80  	},
    81  	{
    82  		Name: "http not https",
    83  		Asset: stellar1.Asset{
    84  			Type:           "credit_alphanum4",
    85  			Code:           "EUR",
    86  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    87  			VerifiedDomain: "keybase.io",
    88  			TransferServer: "http://transfer.keybase.io/transfer",
    89  		},
    90  		MockTransferGet: mockKeybaseTransferGet,
    91  	},
    92  	{
    93  		Name: "ftp not https",
    94  		Asset: stellar1.Asset{
    95  			Type:           "credit_alphanum4",
    96  			Code:           "EUR",
    97  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
    98  			VerifiedDomain: "keybase.io",
    99  			TransferServer: "ftp://transfer.keybase.io/transfer",
   100  		},
   101  		MockTransferGet: mockKeybaseTransferGet,
   102  	},
   103  	{
   104  		Name: "has a query",
   105  		Asset: stellar1.Asset{
   106  			Type:           "credit_alphanum4",
   107  			Code:           "EUR",
   108  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   109  			VerifiedDomain: "keybase.io",
   110  			TransferServer: "https://transfer.keybase.io/transfer?x=123",
   111  		},
   112  		MockTransferGet: mockKeybaseTransferGet,
   113  	},
   114  	{
   115  		Name: "endpoint not found",
   116  		Asset: stellar1.Asset{
   117  			Type:           "credit_alphanum4",
   118  			Code:           "EUR",
   119  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   120  			VerifiedDomain: "keybase.io",
   121  			TransferServer: "https://transfer.keybase.io/nope",
   122  		},
   123  		MockTransferGet: mockKeybaseTransferGet,
   124  	},
   125  	{
   126  		Name: "external url changes domain name",
   127  		Asset: stellar1.Asset{
   128  			Type:           "credit_alphanum4",
   129  			Code:           "EUR",
   130  			Issuer:         "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   131  			VerifiedDomain: "keybase.io",
   132  			TransferServer: "https://transfer.keybase.io/transfer",
   133  		},
   134  		DepositExternalURL:  "https://portal.anchorusd.com/onboarding?account=GBZX4364PEPQTDICMIQDZ56K4T75QZCR4NBEYKO6PDRJAHZKGUOJPCXB&identifier=b700518e7430513abdbdab96e7ead566",
   135  		WithdrawExternalURL: "https://portal.anchorusd.com/onboarding?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI",
   136  		MockTransferGet:     mockKeybaseTransferGet,
   137  	},
   138  	{
   139  		Name: "wwallet unauthorized",
   140  		Asset: stellar1.Asset{
   141  			Type:              "credit_alphanum4",
   142  			Code:              "USD",
   143  			Issuer:            "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   144  			VerifiedDomain:    "www.thewwallet.com",
   145  			TransferServer:    "https://thewwallet.com/ExtApi",
   146  			ShowDepositButton: true,
   147  		},
   148  		MockTransferGet: mockWWTransferGet,
   149  	},
   150  }
   151  
   152  var validAnchorTests = []anchorTest{
   153  	{
   154  		Name: "valid",
   155  		Asset: stellar1.Asset{
   156  			Type:               "credit_alphanum4",
   157  			Code:               "EUR",
   158  			Issuer:             "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   159  			VerifiedDomain:     "www.anchorusd.com",
   160  			TransferServer:     "https://api.anchorusd.com/transfer",
   161  			WithdrawType:       "bank_account",
   162  			ShowWithdrawButton: true,
   163  			ShowDepositButton:  true,
   164  		},
   165  		DepositExternalURL:  "https://portal.anchorusd.com/onboarding?account=GBZX4364PEPQTDICMIQDZ56K4T75QZCR4NBEYKO6PDRJAHZKGUOJPCXB&identifier=b700518e7430513abdbdab96e7ead566",
   166  		WithdrawExternalURL: "https://portal.anchorusd.com/onboarding?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI",
   167  		MockTransferGet:     mockAnchorUSDTransferGet,
   168  	},
   169  	{
   170  		Name: "naobtc",
   171  		Asset: stellar1.Asset{
   172  			Type:              "credit_alphanum4",
   173  			Code:              "BTC",
   174  			Issuer:            "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   175  			VerifiedDomain:    "www.naobtc.com",
   176  			TransferServer:    "https://www.naobtc.com",
   177  			WithdrawType:      "crypto",
   178  			ShowDepositButton: true,
   179  		},
   180  		DepositMessage:  "Deposit request approved by anchor.  19qPSWH6Cytp2zsn4Cntbzz2EMp1fadkRs: 3 confirmations needed. this is long term available address",
   181  		MockTransferGet: mockNaoBTCTransferGet,
   182  	},
   183  	{
   184  		Name: "requires auth",
   185  		Asset: stellar1.Asset{
   186  			Type:               "credit_alphanum4",
   187  			Code:               "EUR",
   188  			Issuer:             "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   189  			VerifiedDomain:     "keybase.io",
   190  			TransferServer:     "https://transfer.keybase.io/transfer",
   191  			AuthEndpoint:       "https://transfer.keybase.io/auth",
   192  			ShowDepositButton:  true,
   193  			ShowWithdrawButton: true,
   194  			DepositReqAuth:     true,
   195  			WithdrawReqAuth:    true,
   196  		},
   197  		MockTransferGet:     mockAuthGet,
   198  		DepositExternalURL:  "https://keybase.io/onboarding?account=GBZX4364PEPQTDICMIQDZ56K4T75QZCR4NBEYKO6PDRJAHZKGUOJPCXB&identifier=b700518e7430513abdbdab96e7ead566",
   199  		WithdrawExternalURL: "https://keybase.io/onboarding?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI",
   200  	},
   201  	{
   202  		Name: "sep24",
   203  		Asset: stellar1.Asset{
   204  			Type:               "credit_alphanum4",
   205  			Code:               "EUR",
   206  			Issuer:             "GAKBPBDMW6CTRDCXNAPSVJZ6QAN3OBNRG6CWI27FGDQT2ZJJEMDRXPKK",
   207  			VerifiedDomain:     "keybase.io",
   208  			TransferServer:     "https://transfer.keybase.io/transfer",
   209  			AuthEndpoint:       "https://transfer.keybase.io/auth",
   210  			ShowDepositButton:  true,
   211  			ShowWithdrawButton: true,
   212  			DepositReqAuth:     true,
   213  			WithdrawReqAuth:    true,
   214  			UseSep24:           true,
   215  		},
   216  		MockTransferGet:     mockAuthGet,
   217  		DepositExternalURL:  "https://keybase.io/transfer/deposit?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI",
   218  		WithdrawExternalURL: "https://keybase.io/transfer/withdraw?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI",
   219  	},
   220  }
   221  
   222  func TestAnchorInteractor(t *testing.T) {
   223  	tc := SetupTest(t, "AnchorInteractor", 1)
   224  	defer tc.Cleanup()
   225  	for i, test := range errAnchorTests {
   226  		accountID, seed := randomStellarKeypair()
   227  		ai := newAnchorInteractor(accountID, &seed, test.Asset)
   228  		ai.httpGetClient = test.MockTransferGet
   229  		_, err := ai.Deposit(tc.MetaContext())
   230  		if err == nil {
   231  			t.Errorf("err test %d [%s]: Deposit returned no error, but expected one", i, test.Name)
   232  			continue
   233  		}
   234  		_, err = ai.Withdraw(tc.MetaContext())
   235  		if err == nil {
   236  			t.Errorf("err test %d [%s]: Withdraw returned no error, but expected one", i, test.Name)
   237  			continue
   238  		}
   239  	}
   240  
   241  	for i, test := range validAnchorTests {
   242  		accountID, seed := randomStellarKeypair()
   243  		ai := newAnchorInteractor(accountID, &seed, test.Asset)
   244  		ai.httpGetClient = test.MockTransferGet
   245  
   246  		// our test tx auth challenges are on the public network:
   247  		if test.Asset.AuthEndpoint != "" {
   248  			stellarnet.SetNetwork(build.PublicNetwork)
   249  			ai.httpPostClient = mockAuthPost
   250  		} else {
   251  			stellarnet.SetNetwork(build.TestNetwork)
   252  		}
   253  
   254  		if test.Asset.ShowDepositButton {
   255  			res, err := ai.Deposit(tc.MetaContext())
   256  			if err != nil {
   257  				t.Errorf("valid test %d [%s]: Deposit returned an error: %s", i, test.Name, err)
   258  				continue
   259  			}
   260  			if res.ExternalUrl == nil && res.MessageFromAnchor == nil {
   261  				t.Errorf("valid test %d [%s] deposit: result fields are all nil", i, test.Name)
   262  				continue
   263  			}
   264  			if test.DepositExternalURL != "" && res.ExternalUrl == nil {
   265  				t.Errorf("valid test %d [%s] deposit: result external url field is nil, expected %s", i, test.Name, test.DepositExternalURL)
   266  				continue
   267  			}
   268  			if res.ExternalUrl != nil {
   269  				if test.DepositExternalURL != *res.ExternalUrl {
   270  					t.Errorf("valid test %d [%s] deposit: result external url field %s, expected %s", i, test.Name, *res.ExternalUrl, test.DepositExternalURL)
   271  				}
   272  			}
   273  			if res.MessageFromAnchor != nil {
   274  				if test.DepositMessage != *res.MessageFromAnchor {
   275  					t.Errorf("valid test %d [%s] deposit: result message %q, expected %q", i, test.Name, *res.MessageFromAnchor, test.DepositMessage)
   276  				}
   277  			}
   278  		}
   279  
   280  		if test.Asset.ShowWithdrawButton {
   281  			res, err := ai.Withdraw(tc.MetaContext())
   282  			if err != nil {
   283  				t.Errorf("valid test %d [%s]: Withdraw returned an error: %s", i, test.Name, err)
   284  				continue
   285  			}
   286  			if res.ExternalUrl == nil && res.MessageFromAnchor == nil {
   287  				t.Errorf("valid test %d [%s] withdraw: result fields are all nil", i, test.Name)
   288  				continue
   289  			}
   290  			if test.WithdrawExternalURL != "" && res.ExternalUrl == nil {
   291  				t.Errorf("valid test %d [%s] withdraw: result external url field is nil, expected %s", i, test.Name, test.WithdrawExternalURL)
   292  				continue
   293  			}
   294  			if res.ExternalUrl != nil {
   295  				if test.WithdrawExternalURL != *res.ExternalUrl {
   296  					t.Errorf("valid test %d [%s] withdraw: result external url field %s, expected %s", i, test.Name, *res.ExternalUrl, test.WithdrawExternalURL)
   297  				}
   298  			}
   299  			if res.MessageFromAnchor != nil {
   300  				if test.WithdrawMessage != *res.MessageFromAnchor {
   301  					t.Errorf("valid test %d [%s] withdraw: result message %q, expected %q", i, test.Name, *res.MessageFromAnchor, test.WithdrawMessage)
   302  				}
   303  			}
   304  		}
   305  	}
   306  }
   307  
   308  // mockKeybaseTransferGet is an httpGetClient func that returns a stored result
   309  // for TRANSFER_SERVER/deposit and TRANSFER_SERVER/withdraw.
   310  func mockKeybaseTransferGet(mctx libkb.MetaContext, url, authToken string) (int, []byte, error) {
   311  	parts := strings.Split(url, "?")
   312  	switch parts[0] {
   313  	case "https://transfer.keybase.io/transfer/deposit":
   314  		return http.StatusForbidden, []byte(depositBody), nil
   315  	case "https://transfer.keybase.io/transfer/withdraw":
   316  		return http.StatusForbidden, []byte(withdrawBody), nil
   317  	default:
   318  		return 0, nil, errors.New("unknown mocked url")
   319  	}
   320  }
   321  
   322  // mockAnchorUSDTransferGet is an httpGetClient func that returns a stored result
   323  // for TRANSFER_SERVER/deposit and TRANSFER_SERVER/withdraw.
   324  func mockAnchorUSDTransferGet(mctx libkb.MetaContext, url, authToken string) (int, []byte, error) {
   325  	parts := strings.Split(url, "?")
   326  	switch parts[0] {
   327  	case "https://api.anchorusd.com/transfer/deposit":
   328  		return http.StatusForbidden, []byte(depositBody), nil
   329  	case "https://api.anchorusd.com/transfer/withdraw":
   330  		return http.StatusForbidden, []byte(withdrawBody), nil
   331  	default:
   332  		return 0, nil, errors.New("unknown mocked url")
   333  	}
   334  }
   335  
   336  // mockNaoBTCTransferGet is an httpGetClient func that returns a stored result
   337  // for TRANSFER_SERVER/deposit and TRANSFER_SERVER/withdraw.
   338  func mockNaoBTCTransferGet(mctx libkb.MetaContext, url, authToken string) (int, []byte, error) {
   339  	parts := strings.Split(url, "?")
   340  	switch parts[0] {
   341  	case "https://www.naobtc.com/deposit":
   342  		return http.StatusOK, []byte(naobtcBody), nil
   343  	case "https://www.naobtc.com/withdraw":
   344  		return http.StatusForbidden, []byte(withdrawBody), nil
   345  	default:
   346  		return 0, nil, errors.New("unknown mocked url")
   347  	}
   348  }
   349  
   350  func mockWWTransferGet(mctx libkb.MetaContext, url, authToken string) (int, []byte, error) {
   351  	parts := strings.Split(url, "?")
   352  	switch parts[0] {
   353  	case "https://thewwallet.com/ExtApi/deposit":
   354  		return http.StatusUnauthorized, []byte("Status Code: 401; Unauthorized"), nil
   355  	default:
   356  		return 0, nil, errors.New("unknown mocked url")
   357  	}
   358  }
   359  
   360  // mockAuthGet is an httpGetClient func that returns a stored result
   361  // for WEB_AUTH_ENDPOINT and TRANSFER_SERVER/deposit and TRANSFER_SERVER/withdraw.
   362  func mockAuthGet(mctx libkb.MetaContext, url, authToken string) (int, []byte, error) {
   363  	parts := strings.Split(url, "?")
   364  	switch parts[0] {
   365  	case "https://transfer.keybase.io/transfer/deposit":
   366  		if authToken == "" {
   367  			return 0, nil, errors.New("missing token")
   368  		}
   369  		return http.StatusForbidden, []byte(authDepositBody), nil
   370  	case "https://transfer.keybase.io/transfer/withdraw":
   371  		if authToken == "" {
   372  			return 0, nil, errors.New("missing token")
   373  		}
   374  		return http.StatusForbidden, []byte(authWithdrawBody), nil
   375  	case "https://transfer.keybase.io/auth":
   376  		return http.StatusOK, []byte(authChallenge), nil
   377  	default:
   378  		return 0, nil, fmt.Errorf("unknown mocked url %q", url)
   379  	}
   380  }
   381  
   382  func mockAuthPost(mctx libkb.MetaContext, url, authToken string, data url.Values) (int, []byte, error) {
   383  	switch url {
   384  	case "https://transfer.keybase.io/auth":
   385  		return 200, []byte(authBody), nil
   386  	case "https://transfer.keybase.io/transfer/transactions/deposit/interactive":
   387  		return 200, []byte(sep24DepBody), nil
   388  	case "https://transfer.keybase.io/transfer/transactions/withdraw/interactive":
   389  		return 200, []byte(sep24WithdrawBody), nil
   390  	default:
   391  		return 0, nil, fmt.Errorf("mockAuthPost: unknown mocked url %q", url)
   392  	}
   393  }
   394  
   395  const depositBody = `{"type":"interactive_customer_info_needed","url":"https://portal.anchorusd.com/onboarding?account=GBZX4364PEPQTDICMIQDZ56K4T75QZCR4NBEYKO6PDRJAHZKGUOJPCXB&identifier=b700518e7430513abdbdab96e7ead566","identifier":"b700518e7430513abdbdab96e7ead566","dimensions":{"width":800,"height":600}}`
   396  const withdrawBody = `{ "type": "interactive_customer_info_needed", "url" : "https://portal.anchorusd.com/onboarding?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI", "id": "82fhs729f63dh0v4" }`
   397  const naobtcBody = `{"how": "19qPSWH6Cytp2zsn4Cntbzz2EMp1fadkRs", "eta": 1800, "extra_info": "3 confirmations needed. this is long term available address", "extra_info_cn": "充值需要三次网络确认。此地址长期有效"}`
   398  const authDepositBody = `{"type":"interactive_customer_info_needed","url":"https://keybase.io/onboarding?account=GBZX4364PEPQTDICMIQDZ56K4T75QZCR4NBEYKO6PDRJAHZKGUOJPCXB&identifier=b700518e7430513abdbdab96e7ead566","identifier":"b700518e7430513abdbdab96e7ead566","dimensions":{"width":800,"height":600}}`
   399  const authWithdrawBody = `{ "type": "interactive_customer_info_needed", "url" : "https://keybase.io/onboarding?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI", "id": "82fhs729f63dh0v4" }`
   400  const authChallenge = `{"transaction":"AAAAAANjzBWOC6YJo49wLshbTPMAmHnZ1I5AESV73e605u3DAAAnEAAAAAAAAAAAAAAAAQAAAABdRIV+AAAAAF1EhqoAAAAAAAAAAQAAAAEAAAAAc35v3HkfCY0CYiA898rk/9hkUeNCTCneeOKQHyo1HJcAAAAKAAAAEFN0ZWxsYXJwb3J0IGF1dGgAAAABAAAAQMCsw7hA+QQnW9t2MfAU92Sqa7eD1udjvaS5BSO9AJFXuELyBmzw+l+GhIry01cM6nz5HKleHf+wDn2jXYYlFKQAAAAAAAAAAbTm7cMAAABAnoRu4cp4cl9UEYqyRIfAIiLhoSU7h77vU9yV2S1RSNZfhc/YaXlMnlLkb9CAeLho1nVMOQnGNzQ55gWJzXXQDQ=="}`
   401  const authBody = `{
   402    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJHQTZVSVhYUEVXWUZJTE5VSVdBQzM3WTRRUEVaTVFWREpIREtWV0ZaSjJLQ1dVQklVNUlYWk5EQSIsImp0aSI6IjE0NGQzNjdiY2IwZTcyY2FiZmRiZGU2MGVhZTBhZDczM2NjNjVkMmE2NTg3MDgzZGFiM2Q2MTZmODg1MTkwMjQiLCJpc3MiOiJodHRwczovL2ZsYXBweS1iaXJkLWRhcHAuZmlyZWJhc2VhcHAuY29tLyIsImlhdCI6MTUzNDI1Nzk5NCwiZXhwIjoxNTM0MzQ0Mzk0fQ.8nbB83Z6vGBgC1X9r3N6oQCFTBzDiITAfCJasRft0z0"
   403  }`
   404  const sep24DepBody = `{ "type": "interactive_customer_info_needed", "url" : "https://keybase.io/transfer/deposit?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI", "id": "82fhs729f63dh0v4" }`
   405  const sep24WithdrawBody = `{ "type": "interactive_customer_info_needed", "url" : "https://keybase.io/transfer/withdraw?account=GACW7NONV43MZIFHCOKCQJAKSJSISSICFVUJ2C6EZIW5773OU3HD64VI", "id": "82fhs729f63dh0v4" }`